Utility functions for working with crypto keys in the browser or node.
This is some helpful functions that make it easier to work with cryptography. Note this does not deal with storing keys. Look at using @bicycle-codes/webauthn-keys (biometric authentication) or indexedDB for help with that.
This includes both sodium based keys and also webcrypto functions.
The Webcrypto keys are preferable because we create them as non-extractable keys, and are able to persist them in indexedDB, despite not being able to read the private key.
Request "persistent" storage with the .persist()
method in the browser.
The install size is kind of large (9.77 MB) because this includes a minified bundle of the sodium library.
Plus, See the docs generated from typescript
npm i -S @bicycle-codes/crypto-util
Use ECC keys with the web crypto API.
Also, you can use RSA keys.
import { create, KeyUse } from '@bicycle-codes/crypto-util'
// create a new keypair
const encryptKeypair = await create(KeyUse.Encrypt)
const signKeys = await create(KeyUse.Sign)
This requires a keypair + another keypair to derive a shared AES key.
import { getSharedKey } from '@bicycle-codes/crypto-util/webcrypto/ecc'
import { KeyUse } from '@bicycle-codes/crypto-util/types'
const alicesKeys = await createEcc(KeyUse.Encrypt)
const bobsKeys = await createEcc(KeyUse.Encrypt)
// pass in our private key, their public key
const sharedKey = await getSharedKey(alicesKeys.privateKey, bobsKeys.publicKey)
Bob can derive the same key by using their private key + Alice's public key.
const bobsSharedKey = await getSharedKey(bobsKeys.privateKey, alicesKeys.publicKey)
Encrypt a given message with a given key.
import { create, encrypt } from '@bicycle-codes/crypto-util/webcrypto/aes'
const aesKey = await create()
const aesEncryptedText = await encrypt('hello AES', aesKey)
import { decrypt } from '@bicycle-codes/crypto-util/webcrypto/aes'
const decrypted = await decrypt(aesEncryptedText, aesKey)
This is a message from Alice to Bob. We use Alice's private key & Bob's public key.
import { KeyUse } from '@bicycle-codes/crypto-util'
import {
create,
encrypt,
decrypt
} from '@bicycle-codes/crypto-util/webcrypto'
const alicesKeys = await create(KeyUse.Encrypt)
const bobsKeys = await create(KeyUse.Encrypt)
const eccEncryptedText = await encrypt(
'hello ecc',
alicesKeys.privateKey,
bobsKeys.publicKey
)
Bob can decrypt the message encrypted by Alice, because we used bob's public key when encrypting it.
// note keys are reversed here --
// alice's public key and bob's private key
const decrypted = await decrypt(
eccEncryptedText,
bobsKeys.privateKey,
alicesKeys.publicKey
)
// => 'hello ecc'
Create another keypair that is used for signatures.
import { KeyUse } from '@bicycle-codes/crypto-util'
import { create } from '@bicycle-codes/crypto-util/webcrypto'
const eccSignKeys = await create(KeyUse.Sign)
import { sign } from '@bicycle-codes/crypto-util/webcrypto/ecc'
const sig = await sign('hello dids', eccSignKeys.privateKey)
A DID is a decentralized identifier, a string the encodes a user's public key.
If you are transmiting your public key along with a message, for example, this is the preferred format.
import { publicKeyToDid } from '@bicycle-codes/crypto-util'
const did = await publicKeyToDid(eccSignKeys.publicKey)
Use a DID to verify a signature string.
import { verifyWithDid, sign } from '@bicycle-codes/crypto-util/ecc'
const sig = await sign('hello dids', eccSignKeys.privateKey)
const isOk = await verifyWithDid('hello dids', sig, did)
This exposes ESM and common JS via package.json exports
field.
import * as util from '@bicycle-codes/crypto-utils'
const util = require('@bicycle-codes/crypto-utils')
This package exposes minified, pre-bundled JS files too. Copy them to a location that is accessible to your web server, then link in HTML.
cp ./node_modules/@bicycle-codes/crypto-util/dist/index.min.js ./public/crypto-util.js
<script type="module" src="./crypto-util.js"></script>
To use the webcrypto API, import from the webcrypto
sub-path.
This depends on an environment with a webcrypto API.
import { aes, ecc, rsa } from '@bicycle-codes/crypto-util/webcrypto'
import { rsa, ecc, aes } from '@bicycle-codes/crypto-util/webcrypto'
// create some ECC keypairs
const eccKeypair = await ecc.create(KeyUse.Sign)
const eccSignKeys = await ecc.create(KeyUse.Encrypt)
// get the public key as a string
const publicKey = await ecc.exportPublicKey(eccSignKeys.publicKey)
// get the public key as a DID format string
const did = await ecc.publicKeyToDid(eccSignKeys.publicKey)
// transform a DID string to a public key instance
const publicKey = ecc.didToPublicKey(eccDid)
aes.create
Create a new AES-GCM key.
function create (opts:{ alg, length } = {
alg: DEFAULT_SYMM_ALGORITHM,
length: DEFAULT_SYMM_LEN
}):Promise<CryptoKey>
aes.create
exampleimport { create } from '@bicycle-codes/crypto-util/webcrypto/aes'
const aesKey = await createAes()
aes.encrypt
Encrypt a string.
async function encrypt (
msg:Msg,
key:SymmKey|string,
opts?:Partial<SymmKeyOpts>
):Promise<string>
import { encrypt } from '@bicycle-codes/crypto-util/webcrypto/aes'
let aesEncryptedText:string
test('encrypt some text with AES', async t => {
aesEncryptedText = await encrypt('hello AES', aesKey)
// returns a string by default
})
aes.decrypt
async function decrypt (
msg:Msg,
key:SymmKey|string,
opts?:Partial<SymmKeyOpts>,
charSize:CharSize = DEFAULT_CHAR_SIZE
):Promise<string>
import { decrypt } from '@bicycle-codes/crypto-util/webcrypto/aes'
const decrypted = await decrypt(aesEncryptedText, aesKey)
// => 'hello AES'
We expose RSA because not all browser yet support ECC keys. See ./src/rsa/webcrypto.ts and ./test/index.ts.
ecc.create
async function create (
use:KeyUse,
curve:EccCurve = EccCurve.P_256,
):Promise<CryptoKeyPair>
create
exampleimport { create } from '@bicycle-codes/crypto-util/webcrypto/ecc'
const alicesEncryptionKeys = await createEcc(KeyUse.Encrypt)
const alicesSigningKeys = await createEcc(KeyUse.Sign)
sign
async function sign (
msg:Msg, // <-- string or Uint8Array
privateKey:PrivateKey,
{ format }:{ format: 'base64'|'raw' } = { format: 'base64' },
charSize:CharSize = DEFAULT_CHAR_SIZE,
hashAlg:HashAlg = DEFAULT_HASH_ALGORITHM,
):Promise<ArrayBuffer|string>
import { sign } from '@bicycle-codes/crypto-util/webcrypto/ecc'
const sig = await sign('hello webcrypto', eccSignKeys.privateKey)
verifyWithDid
Verify a signature with a DID format string.
async function verifyWithDid (
msg:string,
sig:string,
did:DID
):Promise<boolean>
import { verifyWithDid } from '@bicycle-codes/crypto-util/webcrypto/ecc'
const isOk = await verifyWithDid('hello dids', sig, did)
getSharedKey
Get a shared key given two existing keypairs.
async function getSharedKey (
privateKey:PrivateKey,
publicKey:PublicKey,
opts?:Partial<{
alg:'AES-GCM'|'AES-CBC'|'AES-CTR'
length:SymmKeyLength
iv:ArrayBuffer
}>
):Promise<SymmKey>
let BobsKeys:CryptoKeyPair
let sharedKey:CryptoKey
const BobsKeys = await createEcc(KeyUse.Encrypt)
const sharedKey = await getSharedKey(eccKeypair.privateKey, BobsKeys.publicKey)
t.ok(sharedKey instanceof CryptoKey, 'should return a `CryptoKey`')
encrypt
Encrypt something with your private key and the recipient's public key.
async function encrypt (
msg:Msg, // <-- string or Uint8Array
privateKey:PrivateKey,
publicKey:string|PublicKey, // <-- base64 or key
{ format }:{ format: 'base64'|'raw' } = { format: 'base64' },
charSize:CharSize = DEFAULT_CHAR_SIZE,
curve:EccCurve = DEFAULT_ECC_CURVE,
opts?:Partial<{
alg:SymmAlg
length:SymmKeyLength
iv:ArrayBuffer
}>
):Promise<Uint8Array|string>
const eccEncryptedText = await ecc.encrypt(
'hello ecc',
alicesKeys.privateKey,
BobsKeys.publicKey
)
decrypt
Decrypt some text that was encrypted with ecc.ecncrypt
.
async function decrypt (
msg:Msg, // <-- string or Uint8Array
privateKey:PrivateKey,
publicKey:string|PublicKey,
curve:EccCurve = DEFAULT_ECC_CURVE,
opts?:Partial<{
alg:'AES-GCM'|'AES-CBC'|'AES-CTR'
length:SymmKeyLength
iv:ArrayBuffer
}>
):Promise<string>
Note the keys a swapped here -- the public and private keys can come from either keypair and it still works.
const decrypted = await ecc.decrypt(
eccEncryptedText,
BobsKeys.privateKey,
alicesKeys.publicKey
)
These should work anywhere that JS can run.
import { aes, ecc, rsa } from '@bicycle-codes/crypto-util/sodium'
Or import individual modules
import * as aes from '@bicycle-codes/crypto-util/sodium/aes'
import * as ecc from '@bicycle-codes/crypto-util/sodium/ecc'
import * as webcryptoAes from '@bicycle-codes/crypto-util/webcrypto/aes'
Encrypt with AEGIS-256 (symmetric crypto).
import {
create,
encrypt,
decrypt
} from '@bicycle-codes/crypto-util/sodium/aes'
// create a new key
const key = await create()
// or create a key, return a Uint8Array
const keyAsBuffer = await createAes({ format: 'raw' })
// encrypt something
const encryptedString = await encrypt('hello sodium + AES', key)
aes.create
Create a new AES key. Pass { format: 'raw' }
to return a Uint8Array
.
async function create (opts:{
format: 'string'|'raw'
} = { format: 'string' }):Promise<Uint8Array|string>
import { create } from '@bicycle-codes/crypto-util/sodium/aes'
const aesKey = await create()
aes.encrypt
Encrypt the given string or buffer. Pass { format: 'raw' }
to return a Uint8Array
.
async function encrypt (
msg:Uint8Array|string,
key:Uint8Array|string,
opts:Partial<{
iv?:Uint8Array
format?:'string'|'raw'
}> = { format: 'string' },
):Promise<Uint8Array|string>
import { encrypt } from '@bicycle-codes/crypto-util/sodium/aes'
const encryptedString = await encryptAes('hello sodium + AES', aesKey)
aes.decrypt
Decrypt the given string or buffer. Pass { format: 'raw' }
to return a Uint8Array
.
async function decrypt (
cipherText:string|Uint8Array,
key:string|Uint8Array,
opts:{ format:'string'|'raw' } = { format: 'string' }
):Promise<Uint8Array|string>
import { decrypt } from '@bicycle-codes/sodium/aes'
const decrypted = await decrypt(encryptedAes, aesKey)
// => "hello sodium + AES"
import * as ecc from '@bicycle-codes/crypto-util/sodium/ecc'
const keys = await ecc.create()
ecc.create
Create a new Edward keypair.
async function create (
use:KeyUse,
curve:EccCurve = EccCurve.P_256,
):Promise<CryptoKeyPair>
.create
exampleimport { create } from '@bicycle-codes/crypto-util/sodium/ecc'
const keys = await create()
ecc.sign
Create a signature for the diven data. Pass { format: 'raw' }
to get a Uint8Array
instead of a string.
async function sign (
data:string|Uint8Array,
key:LockKey,
opts:{
format:'string'|'raw'
} = { format: 'string' }
):Promise<string|Uint8Array>
ecc.sign
exampleimport { sign } from '@bicycle-codes/crypto-util/webcrypto/ecc'
const sig = await sign('hello webcrypto', alicesKeys, { format: 'raw' })
ecc.publicKeyToDid
Take a public key instance and return a DID format string.
async function publicKeyToDid (
publicKey:Uint8Array|PublicKey
):Promise<DID>
ecc.publicKeyToDid
exampleimport {
exportPublicKey,
publicKeyToDid
} from '@bicycle-codes/crypto-util/webcrypto/ecc'
const arr = await exportPublicKey(eccSignKeys.publicKey)
const did = await publicKeyToDid(arr)
ecc.verify
Verify the given signature + public key + message data.
async function verify (
msg:Msg, // <-- string or Uint8Array
sig:string|Uint8Array|ArrayBuffer,
publicKey:string|PublicKey,
charSize:CharSize = DEFAULT_CHAR_SIZE,
curve:EccCurve = DEFAULT_ECC_CURVE,
hashAlg: HashAlg = DEFAULT_HASH_ALGORITHM
):Promise<boolean>
ecc.verify
exampleconst key = await importDid(eccDid)
const isOk = await verify('hello webcrypto', sig, key)
t.ok(isOk, 'should verify a valid signature')
ecc.verifyWithDid
Verify the given signature + message + public key are ok together, using the given DID string as public key material.
async function verifyWithDid (
msg:string,
sig:string,
did:DID
):Promise<boolean>
ecc.verifyWithDid
exampleimport { verifyWithDid } from '@bicycle-codes/crypto-util/webcrypto/ecc'
const isOk = await verifyWithDid('hello dids', sig, did)
ecc.encrypt
Use the given private and public keys to create a shared key, then encrypt the message with the key.
async function encrypt (
msg:Msg,
privateKey:PrivateKey,
publicKey:string|PublicKey, // <-- base64 or key
{ format }:{ format: 'base64'|'raw' } = { format: 'base64' },
charSize:CharSize = DEFAULT_CHAR_SIZE,
curve:EccCurve = DEFAULT_ECC_CURVE,
opts?:Partial<{
alg:SymmAlg
length:SymmKeyLength
iv:ArrayBuffer
}>
):Promise<Uint8Array|string>
ecc.encrypt example
import { encrypt } from '@bicycle-codes/crypto-util/webcrypto/ecc'
const eccEncryptedText = await encrypt(
'hello ecc',
AlicesKeys.privateKey,
BobsKeys.publicKey
)
ecc.decrypt
Decrypt a message given a public and private key. Note in the example, the keypairs are reversed from the encrypt
example. This creates a new shared key via Diffie Hellman.
import { decrypt } from '@bicycle-codes/crypto-util/webcrypto/ecc'
const decrypted = await decrypt(
eccEncryptedText,
BobsKeys.privateKey,
AlicesKeys.publicKey
)
libsodium
docs
Unless you absolutely need AES-GCM, use AEGIS-256 (crypto_aead_aegis256_*()) instead. It doesn’t have any of these limitations.
indexedDB
.