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.
Tip
Request "persistent" storage with the .persist()
method in the browser.
Note
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
- install
- example
- API
- webcrypto API
- webcrypto AES API
- webcrypto RSA API
- webcrypto ECC API
- sodium API
- Sodium AES API
- Sodium ECC API
- see also
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)
Create a new AES-GCM key.
function create (opts:{ alg, length } = {
alg: DEFAULT_SYMM_ALGORITHM,
length: DEFAULT_SYMM_LEN
}):Promise<CryptoKey>
import { create } from '@bicycle-codes/crypto-util/webcrypto/aes'
const aesKey = await createAes()
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
})
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.
async function create (
use:KeyUse,
curve:EccCurve = EccCurve.P_256,
):Promise<CryptoKeyPair>
import { create } from '@bicycle-codes/crypto-util/webcrypto/ecc'
const alicesEncryptionKeys = await createEcc(KeyUse.Encrypt)
const alicesSigningKeys = await createEcc(KeyUse.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)
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)
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 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 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)
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()
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)
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()
Create a new Edward keypair.
async function create (
use:KeyUse,
curve:EccCurve = EccCurve.P_256,
):Promise<CryptoKeyPair>
import { create } from '@bicycle-codes/crypto-util/sodium/ecc'
const keys = await create()
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>
import { sign } from '@bicycle-codes/crypto-util/webcrypto/ecc'
const sig = await sign('hello webcrypto', alicesKeys, { format: 'raw' })
Take a public key instance and return a DID format string.
async function publicKeyToDid (
publicKey:Uint8Array|PublicKey
):Promise<DID>
import {
exportPublicKey,
publicKeyToDid
} from '@bicycle-codes/crypto-util/webcrypto/ecc'
const arr = await exportPublicKey(eccSignKeys.publicKey)
const did = await publicKeyToDid(arr)
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>
const key = await importDid(eccDid)
const isOk = await verify('hello webcrypto', sig, key)
t.ok(isOk, 'should verify a valid signature')
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>
import { verifyWithDid } from '@bicycle-codes/crypto-util/webcrypto/ecc'
const isOk = await verifyWithDid('hello dids', sig, did)
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>
import { encrypt } from '@bicycle-codes/crypto-util/webcrypto/ecc'
const eccEncryptedText = await encrypt(
'hello ecc',
AlicesKeys.privateKey,
BobsKeys.publicKey
)
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
)
- [question] AES GCM — iv length #74 -- partial motivation for publishing this
libsodium
docsUnless you absolutely need AES-GCM, use AEGIS-256 (crypto_aead_aegis256_*()) instead. It doesn’t have any of these limitations.
- Web Crypto API -- MDN docs
- StorageManager: persist() method
- idb-keyval -- super simple key value storage API built on
indexedDB
.