Skip to content

Create and store asymmetric keys with the webcrypto API in the browser

License

Notifications You must be signed in to change notification settings

bicycle-codes/keys

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

60 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

install

npm i -S @bicycle-codes/keys

get started

indexedDB

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

sign and verify something

.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)

encrypt something

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 }

decrypt something

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)

API

exports

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

ESM

import '@bicycle-codes/keys'

Common JS

require('@bicycle-codes/keys')

pre-built JS

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.

copy

cp ./node_modules/@bicycle-codes/keys/dist/index.min.js ./public/keys.min.js

HTML

<script type="module" src="./keys.min.js"></script>

examples

Create a new Keys instance

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>
}

.create() example

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

const keys = await Keys.create()

Get a hash of the DID

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

static method

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

instance method

class Keys {
  async getDeviceName ():Promise<string>
}

Persist the keys

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.

.persist

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

Restore from indexedDB

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

static .load

class Keys {
    static async load (opts:{
      encryptionKeyName,
      signingKeyName
    } = {
      encryptionKeyName: DEFAULT_ENC_NAME,
      signingKeyName: DEFAULT_SIG_NAME
    }):Promise<Keys>
}

example

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

const newKeys = await Keys.load()

Sign something

Create a new signature for the given input.

class Keys {
  async sign (
    msg:ArrayBuffer|string|Uint8Array,
    charsize?:CharSize,
  ):Promise<Uint8Array>
}

example

const sig = await keys.sign('hello signatures')

Get a signature as a string

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

Verify a signature

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)

Encrypt a key

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>

example

const encrypted = await encryptKeyTo({
    content: myAesKey,
    publicKey: keys.publicEncryptKey
})

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

Encrypt some arbitrary data

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;
}>

example

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
// }

Decrypt a message

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

decryptToString

Decrypt a message, and stringify the result.

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

AES

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

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()

export

Get the AES key as a Uint8Array.

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

exportAsString

Get the key as a string, base64 encoded.

async function exportAsString (key:CryptoKey):Promise<string>
const exported = await AES.exportAsString(aesKey)

encrypt

async function encrypt (
  data:Uint8Array,
  cryptoKey:CryptoKey|Uint8Array,
  iv?:Uint8Array
):Promise<Uint8Array>
const encryptedText = await AES.encrypt(fromString('hello AES'), aesKey)

decrypt

async function decrypt (
  encryptedData:Uint8Array|string,
  cryptoKey:CryptoKey|Uint8Array|ArrayBuffer,
  iv?:Uint8Array
):Promise<Uint8Array>
const decryptedText = await AES.decrypt(encryptedText, aesKey)