@bicycle-codes/keys

keys

tests types module semantic versioning Common Changelog install size license

Create and store keypairs in the browser with the web crypto API.

Use indexedDB to store non-extractable keypairs in the browser. "Non-extractable" means that the browser prevents you from ever reading the private key, but the keys can be persisted and re-used indefinitely.

Tip

Use the persist method to tell the browser not to delete from indexedDB.

See also, the API docs generated from typescript.

Contents

npm i -S @bicycle-codes/keys

Create a new keypair, then save it in indexedDB.

import { Keys } from '@bicycle-codes/keys'

const keys = await Keys.create()

// save the keys to indexedDB
await keys.persist()

// ... sometime in the future ...
// get our keys from indexedDB
const keysAgain = await Keys.load()

console.assrt(keys.DID === keysAgain.DID) // true

.verify takes the content, the signature, and the DID for the public key used to sign. The DID is exposed as the property .DID on a Keys instance.

Note


verify is exposed as a separate function, so you don't have to include all of Keys just to verify a signature.

import { verify } from '@bicycle-codes/keys'

// sign something
const sig = await keys.signAsString('hello string')

// verify the signature
const isOk = await verify('hello string', sig, keys.DID)

Takes the public key we are encrypting to, return an object of { content, key }, where content is the encrypted content as a string, and key is the AES key that was used to encrypt the content, encrypted to the given public key. (AES key is encrypted to the public key.)

import { encryptTo } from '@bicycle-codes/keys'

// need to know the public key we are encrypting for
const publicKey = await keys.getPublicEncryptKey()

const encrypted = await encryptTo.asString({
content: 'hello public key',
publicKey
})

// => { content, key }

A Keys instance has a method decrypt. The encryptedMessage argument is an object of { content, key } as returned from encryptTo, above.

import { Keys } from '@bicycle-codes/keys'

const keys = await Keys.create()
// ...
const decrypted = await keys.decrypt(encryptedMsg)

This exposes ESM and common JS via package.json exports field.

import '@bicycle-codes/keys'
require('@bicycle-codes/keys')

This package exposes minified JS files too. Copy them to a location that is accessible to your web server, then link to them in HTML.

cp ./node_modules/@bicycle-codes/keys/dist/index.min.js ./public/keys.min.js
<script type="module" src="./keys.min.js"></script>

Use the factory function Keys.create because async. The optional parameters, encryptionKeyName and signingKeyName, are added as properties to the keys instance -- ENCRYPTION_KEY_NAME and SIGNING_KEY_NAME. These are used as indexes for saving the keys in indexedDB.

class Keys {
ENCRYPTION_KEY_NAME:string = 'encryption-key'
SIGNING_KEY_NAME:string = 'signing-key'

static async create (opts?:{
encryptionKeyName:string,
signingKeyName:string
}):Promise<Keys>
}
import { Keys } from '@bicycle-codes/keys'

const keys = await Keys.create()

Get a 32-character, DNS-friendly string of the hash of the given DID. Available as static or instance method.

class Keys {
static async deviceName (did:DID):Promise<string>
}
class Keys {
async getDeviceName ():Promise<string>
}

Save the keys to indexedDB. This depends on the values of class properties ENCRYPTION_KEY_NAME and SIGNING_KEY_NAME. Set them if you want to change the indexes under which the keys are saved to indexedDB.

class Keys {
async persist ():Promise<void>
}

Create a Keys instance from data saved to indexedDB. Pass in different indexedDB key names for the keys if you need to.

class Keys {
static async load (opts:{
encryptionKeyName,
signingKeyName
} = {
encryptionKeyName: DEFAULT_ENC_NAME,
signingKeyName: DEFAULT_SIG_NAME
}):Promise<Keys>
}
import { Keys } from '@bicycle-codes/keys'

const newKeys = await Keys.load()

Create a new signature for the given input.

class Keys {
async sign (
msg:ArrayBuffer|string|Uint8Array,
charsize?:CharSize,
):Promise<Uint8Array>
}
const sig = await keys.sign('hello signatures')
class Keys {
async signAsString (
msg:ArrayBuffer|string|Uint8Array,
charsize?:CharSize
):Promise<string>
}
const sig = await keys.signAsString('hello string')
// => ubW9PIjb360v...

Check if a given signature is valid. This is exposed as a stateless function so that it can be used independently from any keypairs. You need to pass in the data that was signed, the signature, and the DID string of the public key used to create the signature.

async function verify (
msg:string|Uint8Array,
sig:string|Uint8Array,
signingDid:DID
):Promise<boolean>
import { verify } from '@bicycle-codes/keys'

const isOk = await verify('hello string', sig, keys.DID)

This method uses async (RSA) encryption, so it should be used to encrypt AES keys only, not arbitrary data. You must pass in either a DID or a public key as the encryption target.

async function encryptKeyTo ({ key, publicKey, did }:{
key:string|Uint8Array|CryptoKey;
publicKey?:CryptoKey|Uint8Array|string;
did?:DID
}):Promise<Uint8Array>
const encrypted = await encryptKeyTo({
content: myAesKey,
publicKey: keys.publicEncryptKey
})

const encryptedTwo = await encryptKeyTo({
content: aesKey,
did: keys.DID
})

Take some arbitrary content and encrypt it. Will use either the given AES key, or will generate a new one if it is not passed in. The return value is the encrypted key and the given data. You must pass in either a DID or a public key to encrypt to.

async function encryptTo (opts:{
content:string|Uint8Array;
publicKey?:CryptoKey|string;
did?:DID;
}, aesKey?:SymmKey|Uint8Array|string):Promise<{
content:Uint8Array;
key:Uint8Array;
}>
import { encryptTo } from '@bicycle-codes/keys'

const encrypted = await encryptTo({
key: 'hello encryption',
publicKey: keys.publicEncryptKey
// or pass in a DID
// did: keys.DID
})

// => {
// content:Uint8Array
// key: Uint8Array <-- the encrypted AES key
// }
class Keys {
async decrypt (msg:{
content:string|Uint8Array;
key:string|Uint8Array;
}):Promise<Uint8Array>
}
const decrypted = await keys.decrypt(encrypted)
// => Uint8Array

Decrypt a message, and stringify the result.

class Keys {
async decryptToString (msg:EncryptedMessage):Promise<string>
}
const decrypted = await keys.decryptToString(encryptedMsg)
// => 'hello encryption'

Expose several AES functions with nice defaults.

  • algorithm: AES-GCM
  • key size: 256
  • iv size: 12 bytes (96 bits)
import { AES } from '@bicycle-codes/keys'

const key = await AES.create(/* ... */)

Create a new AES key. By default uses 256 bits & GCM algorithm.

function create (opts:{ alg:string, length:number } = {
alg: DEFAULT_SYMM_ALGORITHM, // AES-GCM
length: DEFAULT_SYMM_LENGTH // 256
}):Promise<CryptoKey>
import { AES } from '@bicycle-codes/keys'
const aesKey = await AES.create()

Get the AES key as a Uint8Array.

{
async export (key:CryptoKey):Promise<Uint8Array>
}
const exported = await AES.export(aesKey)

Get the key as a string, base64 encoded.

async function exportAsString (key:CryptoKey):Promise<string>
const exported = await AES.exportAsString(aesKey)
async function encrypt (
data:Uint8Array,
cryptoKey:CryptoKey|Uint8Array,
iv?:Uint8Array
):Promise<Uint8Array>
const encryptedText = await AES.encrypt(fromString('hello AES'), aesKey)
async function decrypt (
encryptedData:Uint8Array|string,
cryptoKey:CryptoKey|Uint8Array|ArrayBuffer,
iv?:Uint8Array
):Promise<Uint8Array>
const decryptedText = await AES.decrypt(encryptedText, aesKey)