
Product
Introducing Tier 1 Reachability: Precision CVE Triage for Enterprise Teams
Socket’s new Tier 1 Reachability filters out up to 80% of irrelevant CVEs, so security teams can focus on the vulnerabilities that matter.
@ssc-half-light/identity
Advanced tools
This is an object representing a user. An Identity object contains a collection of "devices", where each device has several keypairs. This depends on each device having a keystore that stores the private keys.
We can do e2e encryption by creating a symmetric key, then encrypting that key to each device. So the symmetric key is encrypted with the public key of each device.
Each device has a primary keypair used for signing, which is did
here, and also an "exchange" keypair, which is used for encrypting & decrypting things. In the Device
record there is also an index aes
, which is the symmetrical key that has been encrypted to the device's exchange key.
see also, keystore as used in crypto component
Devices are indexed by a sufficiently random key, created by calling createDeviceName 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 exchange keys of the other party.
npm i -S @ssc-half-light/identity
See a live demo of the example directory
This uses websockets to 'link' two devices. That is, a single AES key is encrypted to the exchange key on each device, so both devices are able to use the same key.
interface Identity {
humanName:string, /* a human readble name for the identity */
username:string, /* the random string for the root device.
Not human-readable */
rootDid:DID /* `did:key:z${string}`
The DID of the first device to use this identity */
devices:Record<string, Device> /* a map of devices in this identity */
}
interface Device {
name:string, // the random string for this device
did:DID, // `did:key:z${string}`
aes:string, /* the symmetric key for this account, encrypted to the
exchange key for this device */
exchange:string // public key used for encrypting & decrypting
}
A function from data to an encrypted string.
type Group = {
groupMembers: Identity[];
encryptedKeys: Record<string, string>;
decrypt: (
crypto:Implementation,
group:Group,
msg:string|Uint8Array
) => Promise<string>;
(data:string|Uint8Array): Promise<string>
}
Start the example. This will start local servers and open a browser.
npm start
The example opens a websocket connection to our partykit server in response to DOM events. We generate a random 6 digit number, and use that to connect multiple devices to the same websocket server. The root device (the one that generated the PIN) will get a message from the new device, containing the exchange public key and DID. The root device then encrypts the AES key to the exchange key in the message, and then sends the encrypted AES key back to the new device over the websocket.
After that both machines have the same AES key, so are able to read & write the same data.
This is storage agnostic. You would want to save the identity object to a database or something, which is easy to do because keys are encrypted "at rest". Any device record pairs with a keystore instance on the device.
We are not using any env variables. If you use an env variable, deploy to partykit like this:
npx partykit deploy --with-vars
There is an env variable, PARTYKIT_TOKEN
, on github. This is for deploying partykit automatically on any github push. It's not used by our app.
Tests run in node because we are using @ssc-hermes/node-components.
npm test
Import functions and types
import { test } from '@nichoth/tapzero'
import { writeKeyToDid, DID } from '@ssc-half-light/util'
import {
components,
createCryptoComponent
} from '@ssc-hermes/node-components'
import { Crypto } from '@oddjs/odd'
import { aesEncrypt, aesDecrypt } from
'@oddjs/odd/components/crypto/implementation/browser'
import { fromString, toString } from 'uint8arrays'
import {
create, decryptKey, Identity, ALGORITHM, add,
createDeviceName, encryptTo, CurriedEncrypt,
decryptMsg
} from '@ssc-half-light/identity'
Convenient helpers that will encode and decode strings with base64pad
format.
import { arrayBuffer } from '@ssc-half-light/identity'
const { fromString, toString } = arrayBuffer
Create an identity
let identity:Identity
let rootDid:DID
let crypto:Crypto.Implementation
let rootDeviceName:string
test('create an identity', async t => {
// ...get an odd program somehow
crypto = program.components.crypto
rootDid = await writeKeyToDid(crypto)
identity = await create(crypto, {
humanName: 'alice',
})
const deviceName = await createDeviceName(rootDid)
rootDeviceName = deviceName
t.ok(identity, 'should return a new identity')
t.ok(identity.devices[deviceName].aes,
'should map the symmetric key, indexed by device name')
})
Decrypt the given encrypted AES key.
async function decryptKey (
crypto:Crypto.Implementation,
encryptedKey:string
):Promise<CryptoKey>
const aes = identity.devices[rootDeviceName].aes
const decryptedKey = await decryptKey(crypto, aes)
Use the decrypted key to read and write
import { aesDecrypt, aesEncrypt } from '@ssc-half-light/identity'
test('can use the keys', async t => {
// test that you can encrypt & decrypt with the symmetric key
// saved in identity
// first decrypt the key
const aes = identity.devices[rootDeviceName].aes
const decryptedKey = await decryptKey(crypto, aes)
t.ok(decryptedKey instanceof CryptoKey, 'decryptKey should return a CryptoKey')
// now use it to encrypt a string
const encryptedString = await aesEncrypt(
fromString('hello'), decryptedKey, ALGORITHM)
t.ok(encryptedString instanceof Uint8Array,
'should return a Uint8Array when you encrypt a string')
// now decrypt the string
const decrypted = toString(
await aesDecrypt(encryptedString, decryptedKey, ALGORITHM)
)
t.equal(decrypted, 'hello', 'can decrypt the original string')
})
Encrypt a given AES key to a given exchange key. You mostly should not need to use this.
/**
* Encrypt a given AES key to the given exchange key
* @param key The symmetric key
* @param exchangeKey The exchange key to encrypt *to*
* @returns the encrypted key, encoded as 'base64pad'
*/
export async function encryptKey (
key:CryptoKey,
exchangeKey:Uint8Array|CryptoKey
):Promise<string>
Add a device to this identity.
We need to pass in the crypto
object from the original identity, because we need to decrypt the secret key, then re-encrypt it to the new device:
// decrypt the AES key
const secretKey = await decryptKey(
crypto,
id.devices[existingDeviceName].aes
)
We need to call this function from the existing device, because we need to decrypt the AES key. We then re-encrypt the AES key to the public exchange key of the new device. That means we need to get the exchangeKey
of the new device somehow.
test('add a device to the identity', async t => {
const device2Crypto = await createCryptoComponent()
const newDid = await writeKeyToDid(device2Crypto)
const exchangeKey = await device2Crypto.keystore.publicExchangeKey()
// add the device. Returns the ID with the new device added
// NOTE this takes params from the original keypair -- `crypto`
// and also params from the new keypair -- `exchangeKey`
const id = await add(identity, crypto, newDid, exchangeKey)
t.ok(id, 'should return a new identity')
const newDeviceName = await createDeviceName(newDid)
t.ok(identity.devices[newDeviceName],
'new identity should have a new device with the expected name')
t.ok(identity.devices[rootDeviceName],
'identity should still have the original device')
})
Encrypt a message to the given set of identities. To decrypt this message, use your exchange key to decrypt the AES key, then use the AES key to decrypt the payload.
/**
* This creates a new AES key each time it is called.
*
* @param crypto odd crypto object
* @param ids The Identities we are encrypting to
* @param data The message we want to encrypt
*/
export async function encryptTo (
creator:Identity,
ids:Identity[],
data?:string|Uint8Array
):Promise<EncryptedMessage | CurriedEncrypt>
encryptTo
example// a message from alice to bob
const encryptedMsg = await encryptTo(alice, [bob], 'hello bob')
const alice = await create(alicesCrypto, {
humanName: 'alice'
})
const bob = await create(bobsCrypto, {
humanName: 'bob'
})
encryptTo
encryptTo
can be partially applied by calling without the last argument, the message.
const encryptedGroup = await encryptTo(alice, [
bob,
carol
]) as CurriedEncrypt
Decrypt a message. Takes an encrypted message, and returns the decrypted message body.
async function decryptMsg (
crypto:Crypto.Implementation,
encryptedMsg:EncryptedMessage
):Promise<string>
decryptMsg
exampleconst newMsg = await encryptTo(alice, [bob], 'hello bob') as EncryptedMessage
t.ok(newMsg.payload, 'Encrypted message should have payload')
const newDecryptedMsg = await decryptMsg(bobsCrypto, newMsg)
t.equal(newDecryptedMsg, 'hello bob',
'Bob can decrypt a message encrypted to bob')
Create a group of identities that share a single AES key.
This will return a new function that encrypts data with the given key.
This differs from encryptTo
, above, because this takes an existing key, instead of creating a new one.
export type Group = {
groupMembers: Identity[];
encryptedKeys: Record<string, string>;
decrypt: (
crypto:Crypto.Implementation,
group:Group,
msg:string|Uint8Array
) => Promise<string>;
(data:string|Uint8Array): Promise<string>
}
/**
* Create a group with the given AES key.
*
* @param creator The identity that is creating this group
* @param ids An array of group members
* @param key The AES key for this group
* @returns {Promise<Group>} Return a function that takes a string of
* data and returns a string of encrypted data. Has keys `encryptedKeys` and
* `groupMemebers`. `encryptedKeys` is a map of `deviceName` to the
* encrypted AES key for this group. `groupMembers` is an array of all
* the Identities in this group.
*/
export async function group (
creator:Identity,
ids:Identity[],
key:CryptoKey
):Promise<Group>
group
exampleimport { group } from '@ssc-half-light/identity'
// bob and carol are instances of Identity
const myGroup = await group(alice, [bob, carol], key)
Decrypt a message that has been encrypted to the group.
async function Decrypt (
group:Group,
crypto:Crypto.Implementation,
msg:string|Uint8Array
):Promise<string>
group.Decrypt
exampleimport { group } from '@ssc-half-light/identity'
const myGroup = await group(alice, [bob, carol], key)
const groupMsg = await myGroup('hello group')
const msg = await group.Decrypt(alicesCrytpo, myGroup, groupMsg)
// => 'hello group'
Add another identity to a group, and return a new group (not the same instance).
If you pass in a Crypto.Implementation
instance, then we will use that to decrypt the key of the given group.
If you pass in an AES CryptoKey
, it will be encrypted to the new user. It should be the same AES key that is used by the group.
async function AddToGroup (
group:Group,
keyOrCrypto:CryptoKey|Implementation,
newGroupMember:Identity,
):Promise<Group>
AddToGroup
exampleimport { AddToGroup, create } from '@ssc-half-light/identity'
const fran = await create(_crypto, {
humanName: 'fran'
})
const newGroup = await AddToGroup(myGroup, alicesCrytpo, fran)
Create a URL-friendly hash string for a device. This is 32 characters of a hash for a given device's DID. It will always return the same string for the same DID/device.
Pass in a crypto
instance or DID string
async function getDeviceName (input:DID|Crypto.Implementation):Promise<string>
getDeviceName
examplePass in a crypto
instance
import { getDeviceName } from '@ssc-half-light/identity'
const myDeviceName = getDeviceName(program.components.crypto)
// => '4k4z2xpgpmmssbcasqanlaxoxtpppl54'
Pass in a DID as a string
import { getDeviceName } from '@ssc-half-light/identity'
const deviceName = getDeviceName('did:key:z13V3Sog2Y...')
// => '4k4z2xpgpmmssbcasqanlaxoxtpppl54'
FAQs
An identity record
The npm package @ssc-half-light/identity receives a total of 44 weekly downloads. As such, @ssc-half-light/identity popularity was classified as not popular.
We found that @ssc-half-light/identity demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
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.
Product
Socket’s new Tier 1 Reachability filters out up to 80% of irrelevant CVEs, so security teams can focus on the vulnerabilities that matter.
Research
/Security News
Ongoing npm supply chain attack spreads to DuckDB: multiple packages compromised with the same wallet-drainer malware.
Security News
The MCP Steering Committee has launched the official MCP Registry in preview, a central hub for discovering and publishing MCP servers.