Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@bicycle-codes/identity

Package Overview
Dependencies
Maintainers
0
Versions
34
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@bicycle-codes/identity

An identity record

  • 0.9.9
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
42
decreased by-79.1%
Maintainers
0
Weekly downloads
 
Created
Source

identity

tests module types semantic versioning license

Use non-extractable keypairs as user identity.


Use the webcrypto API to create keypairs representing a user.

All encryption is via AES-GCM.

All asymmetric crypto is using RSA, because we are waiting for more browsers to support ECC.


contents


conceptual view

Create two keypairs -- 1 for signing and 1 for encrypting, and store them in indexedDB in the browser. All keypairs here are "non-extractable", so you are never able to read the private key, but they still persist indefinitely in indexedDB.

We can do passwordless user ID, using something like UCAN to link multiple devices if you want.

E2E encryption

We can do e2e encryption by creating a symmetric key, then encrypting that key to each device that should be able to read the message. So the symmetric key is encrypted with the public key of each device.

Devices are indexed by a sufficiently random key, created by calling getDeviceName with the primary did for the device.


Sending a private message to an identity would mean encrypting a message with a new symmetric key, then encrypting n versions of the symmetric key, one for each device in the other identity.

You can think of it like one conversation = 1 symmetric key. The person initiating the conversation needs to know the encryption keys of the other party.


install


npm i -S @bicycle-codes/identity

use

import { Identity } from '@bicycle-codes/identity'

const id = await Identity.create({
    humanName: 'alice',
    humanReadableDeviceName: 'phone'
})

quick example


Given two identities, create a message that is readble by them only.

import type { EncryptedMessage } from '@bicycle-codes/identity'
import { Identity } from '@bicycle-codes/identity'

// get identities somehow
const alice = await Identity.create(crypto, {
    humanName: 'alice',
})
const bob = await Identity.create(bobsCrypto, {
    humanName: 'bob'
})

const msgToBob = await alice.encryptMsg('hello bob', [
    await bob.serialize()
])

//  __the encrypted message__
//
// => {
//     payload:string, /* This is the message, encrypted with the AES key for
//         this message */
//     devices:Record<string, string> <-- A map from device name to AES key,
//          encrypted to the device
// }
//

// bob can read the message b/c they are passed in as a recipient above
const decrypted = await bob.decryptMsg(msgToBob)
   // => 'hello bob'

API


See bicycle-codes.github.io/identity for complete API docs.

globals

We use some "global" keys in indexedDB and localStorage. These can be configured by setting class properties.

indexedDB
  • encryption-key -- RSA key for encrypt/decrypt
  • signing-key -- RSA key for signatures

Set the class properties ENCRYPTION_KEY_NAME and SIGNING_KEY_NAME to configure this.

class Identity {
    static ENCRYPTION_KEY_NAME:string = 'encryption-key'
    static SIGNING_KEY_NAME:string = 'signing-key'
}
localStorage
  • identity -- store a serialized Identity here, when you call save.

Configure this with the class property STORAGE_KEY.

class Identity {
    static STORAGE_KEY:string = 'identity'
}

import


Import functions and types

import {
    type EncryptedMessage,
    Identity,
    encryptKey,
    aesExportKey,
    decryptKey,
    aesGenKey,
    aesDecrypt,
    encryptContent,
    exportPublicKey,
    verifyFromString,
    getDeviceName
} from '@bicycle-codes/identity'

create

Use this factory function, not the constructor, because it is async.

By default this will store keypairs in indexedDB with the keys encryption-key and signing-key. Set the class properties ENCRYPTION_KEY_NAME and SIGNING_KEY_NAME to change these.

class Identity {
    static async create (opts:{
        humanName:string;
        type?: 'rsa';
        humanReadableDeviceName:string;  // a name for this device
    }):Promise<Identity>
}
example
const alice = await Identity.create({
    humanName: 'alice',
    humanReadableDeviceName: 'phone'
})

save

Save an existing Identity to localStorage.

By default this saves to the localStorage key identity. Set the class property STORAGE_KEY to change the storage key.

class Identity {
    static STORAGE_KEY:string = 'identity'

    static save (id:SerializedIdentity) {
        localStorage.setItem(Identity.STORAGE_KEY, JSON.stringify(id))
    }
}
example
import { Identity } from '@bicycle-codes/identity'

// `alice` is an id we created earlier
Identity.save(await alice.serialize())

init

Load an Identity that has been saved in localStorage & indexedDB.

class Identity {
    static async init (opts:{
        type?:'rsa';
        encryptionKeyName:string;
        signingKeyName:string;
    } = {
        encryptionKeyName: DEFAULT_ENCRYPTION_KEY_NAME,
        signingKeyName: DEFAULT_SIGNING_KEY_NAME
    }):Promise<Identity>
}
example
import { Identity } from '@bicycle-codes/identity'

const alice = await Identity.init()

serialize

Return a JSON stringifiable version of this Identity.

class Identity {
    async serialize ():Promise<SerializedIdentity>
}

getDeviceName

Create a 32-character, DNS-friendly hash for a device. Takes either the DID string or a CryptoKeyPair.

/**
 * Get the device name -- a 32 character, DNS-friendly name
 *
 * @returns {Promise<string>}
 */
class Identity {
    getDeviceName ():Promise<string>
}
example
const alice = Identity.create({
    humanName: 'alice',
    humanReadableDeviceName: 'phone'
})

alice.getDeviceName()
// => qfcip23vxaiprwmbyo3dxyrurltt4rgo

sign

Sign the given message with the RSA signingKey; return a Uint8Array.

class Identity {
    sign (msg:Msg, charsize?:CharSize):Promise<Uint8Array>
}

signAsString

Sign the given message with the RSA signingKey; return a string.

class Identity {
    signAsString (msg:string):Promise<string>
}

static createDeviceRecord

Create a new Device record. This means creating asymmetric keypairs for the device, and storing them in indexedDB. This does not include an AES key in the device record, because typically you create a device record before adding the device to a different Identity, so you would add an AES key at that point.

This function does read the class properties ENCRYPTION_KEY_NAME and SIGNING_KEY_NAME, because it creates asymmetric keys for the device and saves them in localStorage.

class Identity {
    static async createDeviceRecord (opts:{
        humanReadableName:string
    }):Promise<Omit<Device, 'aes'>> {
}

encryptMsg

Each new message gets a new AES key. The key is then encrypted to the public key of each recipient.

class Identity {
    async encryptMsg (
        data:string|Uint8Array,
        recipients?:SerializedIdentity[],
    ):Promise<EncryptedMessage>
}

decryptMsg

The given message should include an AES key, encrypted to this device. Look up the AES key by device name, and use it to decrypt the message.

class Identity {
    async decryptMsg (encryptedMsg:EncryptedMessage):Promise<string>
}

addDevice

Add a new device to this Identity. Returns this.

class Identity {
    async addDevice (opts:Omit<Device, 'aes'>):Promise<Identity>
}
example
import { Identity } from '@bicycle-codes/identity'

const alice = Identity.create({ /* ... */ })

// ... need to get the other device record somehow ...
const workComputer:Device = // ...

await alice.addDevice(workComputer)

types


Device

interface Device {
    name:string;  // <-- random, collision resistant name
    humanReadableName:string;
    did:DID;
    aes:string;  /* <-- the symmetric key for this account, encrypted to the
      exchange key for this device */
    encryptionKey:string;  // <-- encryption key, stringified
}

SerializedIdentity

interface SerializedIdentity {
    humanName:string;
    username:string;
    DID:DID;
    rootDID:DID;
    rootDeviceName:string;
    devices:Record<string, Device>;
    storage:{ encryptionKeyName:string; signingKeyName:string; }
}

Msg

type Msg = ArrayBuffer|string|Uint8Array

EncryptedMessage

Each new message gets a new AES key. The key is then encrypted to the public key of each recipient.

interface EncryptedMessage<T extends string = string> {
    payload:T, /* This is the message, encrypted with the symm key for
        the message */
    devices:Record<string, string>  /* a map from `deviceName` to this
        messages's encrypted AES key, encrypted to that device */
}

z


This package exposes some type checking utilities made with zod. Import z:

import { device, SerializedIdentity, did  } from '@bicycle-codes/identity/z'

test


Tests run in a browser environment via tape-run.

npm test

Keywords

FAQs

Package last updated on 02 Oct 2024

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc