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