Link multiple devices via websocket. Linking means that both devices share the same AES key.
This depends on each device having a keystore that stores the private keys. Also, you need a websocket server, for example partykit.
We have two devices, a parent and a child. To securely send an AES key to another device, the parent first opens a websocket connection at a random URL. The URL for the websocket needs to be transmitted out-of-band.
When the new device (the child) connects to the websocket, it tells the parent its public key. The parent then encrypts its AES key to the child's public key. The child gets the key, which it is able to decrypt with its private key.
npm i -S @bicycle-codes/link
import { Parent, Child } from '@bicycle-codes/link'
import type { Certificate, NewDeviceMessage } from '@bicycle-codes/link'
Connect two devices, a phone and computer, for example. They must both know code
, which by default is a 6 digit numberic code. The code must be transmitted out of band.
import { program as Program } from '@oddjs/odd'
import { create as createID } from '@bicycle-codes/identity'
import { Parent } from '@bicycle-codes/link'
const program = await Program({
namespace: {
name: 'link-example',
creator: 'bicycle-computing'
}
})
const { crypto } = program.components
const myId = await createID(crypto, {
humanName: 'alice',
humanReadableDeviceName: 'phone'
})
/**
* 'phone' is the parent device. The parent should connect first.
* The resolved promise is for a new `Identity`, which is a new ID, including
* the child device
*/
const newIdentity = await Parent(myId, crypto, {
host: 'localhost:1999',
code: '1234'
})
...On a different machine...
import { program as Program } from '@oddjs/odd'
import { Child } from '@bicycle-codes/link'
const program = await Program({
namespace: {
name: 'link-example',
creator: 'bicycle-computing'
}
})
const { crypto } = program.components
const { identity, certificate } = await Child(crypto, {
host: PARTY_URL,
code: '1234',
humanReadableDeviceName: 'computer'
})
Both machines now have an ID that looks like this:
{
"username": "vnhq32ybnanplsklhfd2cd6cdqaoeygl",
"humanName": "alice",
"rootDID": "did:key:z13V3Sog2YaU...",
"devices": {
"vnhq32ybnanplsklhfd2cd6cdqaoeygl": {
"aes": "Cj1XnlPQA35VroF...",
"name": "vnhq32ybnanplsklhfd2cd6cdqaoeygl",
"humanReadableName": "phone",
"did": "did:key:z13V3Sog2Y...",
"exchange": "MIIBIjANBgkqhkiG..."
},
"5ngvlbhsrfvpua3qnhllakwnnd2tzwzo": {
"name": "5ngvlbhsrfvpua3qnhllakwnnd2tzwzo",
"humanReadableName": "computer",
"aes": "oAbLoAtJawSbA3r2tI4BDEmb...",
"did": "did:key:z13V3Sog2YaUKhdGCmg...",
"exchange": "MIIBIjANBgkqhkiG9w0BAQEFA..."
}
}
}
This depends on a websocket server existing. We provide the export
server
to help with this.
This should be ergonomic to use with partykit.
import type * as Party from 'partykit/server'
import { onConnect, onMessage } from '@bicycle-codes/link/server'
export default class Server implements Party.Server {
existingDevice:string|undefined
constructor (readonly room: Party.Room) {
this.room = room
}
/**
* Parent device must connect first
*/
onConnect (conn:Party.Connection) {
onConnect(this, conn)
}
onMessage (message:string, sender:Party.Connection) {
onMessage(this, message, sender)
}
}
Server satisfies Party.Worker
Call this from the "parent" device. It returns a promise that will resolve with a new identity, that includes the child devices.
import type { Crypto, Identity } from '@bicycle-codes/identity'
async function Parent (identity:Identity, oddCrypto:Crypto, {
host,
code,
query
}:{
host:string;
code:string;
query?:string;
}):Promise<Identity>
Call this from the "child" device. It returns a promise that will resolve with
{ identity, certificate }
, where certificate
is a signed message from the
parent device, serving as proof that the child is authorized.
import type { Crypto, Identity } from '@bicycle-codes/identity'
async function Child (oddCrypto:Crypto, {
host,
code,
query,
humanReadableDeviceName
}:{
host:string;
code:string;
query?:string;
humanReadableDeviceName:string;
}):Promise<{ identity:Identity, certificate:Certificate }>
Need to create a code before connecting the parent device. The code should be transmitted out-of-band; it serves as verification that the two devices want to connect.
By default this will create a random 6 digit numeric code. Internally we are using nanoid to create the code.
To create your own code, use the nanoid-dictionary package.
function Code (alphabet?:string, length?:number):string {
return customAlphabet(alphabet || numbers, length ?? 6)()
}
import { Code } from '@bicycle-codes/link'
const code = Code()
// => 942814
Pass in a dictionary and the desired length of the code.
import { Code } from '@bicycle-codes/link'
import { alphanumeric } from 'nanoid-dictionary'
function myCodeGenerator () {
// return a 10 character, alphanumeric random code
return Code(alphanumeric, 10)
}
The certificate is a signed message from the "parent" device, saying that the new device is authorized.
import { create as createMessage } from '@bicycle-codes/message'
type Certificate = Awaited<
ReturnType<typeof createMessage<{
exp?:number; /* <-- Expiration, unix timestamp,
after which this certificate is no longer valid.
Default is no expiration. */
nbf?:number /* <-- Not Before, unix timestamp of when the certificate
becomes valid. */
recipient:DID // <-- DID of who this certificate is intended for
}>>
>
A message from the new, "child" device
export type NewDeviceMessage = {
newDid:`did:key:z${string}`; // <-- DID for the new device
deviceName:string; // <-- the auto generated random string
exchangeKey:string;
humanReadableDeviceName:string; // <-- a name for the new device
}
The certificate will also have keys author
and signature
, via the
message module, with the DID and
signature for this data.