Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
nichoth committed Apr 21, 2024
1 parent 582b24a commit 036e307
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 15 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

Link multiple devices via websocket.

This depends on each device having a [keystore](https://github.com/fission-codes/keystore-idb) that holds its private keys.

## install

```sh
Expand Down
14 changes: 14 additions & 0 deletions example/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { html } from 'htm/preact'
import { render } from 'preact'

example()

const PARTY_URL = (import.meta.env.DEV ?
'localhost:1999' :
'link.nichoth.partykit.dev')

function Example () {
return html`<div>hello</div>`
}

render(html`<${Example} />`, document.getElementById('root')!)
10 changes: 0 additions & 10 deletions example/index.tsx

This file was deleted.

9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
"postversion": "git push --follow-tags && npm publish",
"prepublishOnly": "npm run build"
},
"dependencies": {
"@bicycle-codes/identity": "^0.7.2",
"@bicycle-codes/message": "^0.7.2",
"partysocket": "^1.0.1",
"uint8arrays": "^5.0.3"
},
"devDependencies": {
"@bicycle-codes/tapzero": "^0.9.2",
"@nichoth/debug": "^0.6.7",
Expand All @@ -30,12 +36,13 @@
"esbuild": "^0.20.2",
"eslint": "^8.57.0",
"eslint-config-standard": "^17.1.0",
"htm": "^3.1.1",
"postcss-nesting": "^12.1.1",
"preact": "^10.20.2",
"tap-spec": "^5.0.0",
"tape-run": "^11.0.0",
"typescript": "^5.4.4",
"vite": "^5.1.6"
"vite": "^5.2.10"
},
"exports": {
".": {
Expand Down
165 changes: 161 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,163 @@
import { createDebug } from '@nichoth/debug'
const debug = createDebug()
import { PartySocket } from 'partysocket'
import { toString } from 'uint8arrays'
import { create as createMessage } from '@bicycle-codes/message'
import {
writeKeyToDid,
addDevice,
createDeviceName
} from '@bicycle-codes/identity'
import type { DID, Crypto, Identity } from '@bicycle-codes/identity'

export function example ():void {
debug('hello')
/**
* Message from the new, incoming, 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 is a signed message from the "parent" device,
* saying that the new device is authorized.
*/
export type Certificate = Awaited<
ReturnType<typeof createMessage<{
exp?:number; /* <-- Expiration, unix timestamp,
after which this certificat 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
}>>
>

/**
* Open a websocket channel that the new device should connect to.
*
* @param {Identity} identity The existing identity
* @param {Crypto} oddCrypto A Crypto implementation from `odd`
* @param {Object} opts Host, crypto, and a code for the websocket
* @param {Crypto} opts.oddCrypto An instance of odd crypto
* @param {string} opts.host The address for your websocket
* @param {string} opts.code A unique ID for the websocket connection. Should
* be transmitted out of band to the new device.
* @returns {Promise<Identity>} A promise that will resolve with a new identity
* instance that includes the new device, after we get a message from the
* new device.
*/
export function Parent (identity:Identity, oddCrypto:Crypto, {
host,
code,
id,
query
}:{
host:string;
code:string;
id?:string;
query?:string;
}):Promise<Identity> {
const party = new PartySocket({
host,
room: code,
id,
query: {
token: query
}
})

return new Promise((resolve, reject) => {
party.addEventListener('message', async ev => {
let msg:NewDeviceMessage
let newDid:DID, exchangeKey:string, humanReadableDeviceName:string

try {
msg = JSON.parse(ev.data);
({
newDid,
exchangeKey,
humanReadableDeviceName
} = msg)
} catch (err) {
return reject(err)
}

const newIdentity = await addDevice(
identity,
oddCrypto,
newDid,
exchangeKey,
humanReadableDeviceName
)

const certificate:Certificate = await createMessage<{
recipient:DID
}>(
oddCrypto,
{ recipient: newDid }
)

party.send(JSON.stringify({
newIdentity,
certificate
}))

resolve(newIdentity)

// we are done now
party.close()
})
})
}

export async function Child (oddCrypto:Crypto, {
host,
code,
query,
humanReadableDeviceName
}:{
host:string;
code:string;
query?:string;
humanReadableDeviceName:string;
}) {
const party = new PartySocket({
host,
room: code,
query: { token: query },
})

const newDid = await writeKeyToDid(oddCrypto)
const deviceName = await createDeviceName(newDid)

/**
* Send our DID to the existing device
*/
party.send(JSON.stringify({
deviceName,
humanReadableDeviceName,
newDid: await writeKeyToDid(oddCrypto),
exchangeKey: toString(
await oddCrypto.keystore.publicExchangeKey()
)
}))

return new Promise((resolve, reject) => {
/**
* We should only get 1 message,
* containing the new identity that includes this device,
* and the certificate that authorizes this device
*/
party.addEventListener('message', async (ev) => {
let newIdentity:Identity, certificate:Certificate
try {
({ newIdentity, certificate } = JSON.parse(ev.data))
} catch (err) {
return reject(err)
}

resolve({ newIdentity, certificate })
})
})
}
33 changes: 33 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { DID, Crypto } from '@bicycle-codes/identity'
import { concat, toString } from 'uint8arrays'

const BASE58_DID_PREFIX = 'did:key:z'

export async function writeKeyToDid (
crypto:Crypto
):Promise<DID> {
const [pubKey, ksAlg] = await Promise.all([
crypto.keystore.publicWriteKey(),
crypto.keystore.getAlgorithm()
])

return publicKeyToDid(crypto, pubKey, ksAlg)
}

export function publicKeyToDid (
crypto:Crypto,
publicKey:Uint8Array,
keyType:string
):DID {
// Prefix public-write key
const prefix = crypto.did.keyTypes[keyType]?.magicBytes
if (prefix === null) {
throw new Error(`Key type '${keyType}' not supported, ` +
`available types: ${Object.keys(crypto.did.keyTypes).join(', ')}`)
}

const prefixedBuf = concat([prefix, publicKey])

// Encode prefixed
return (BASE58_DID_PREFIX + toString(prefixedBuf, 'base58btc')) as DID
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"listFiles": true,
"module": "NodeNext",
"target": "ES2022",
"types": ["vite/client"],
"moduleResolution": "nodenext",
"esModuleInterop": false,
"lib": ["ES2022", "DOM", "WebWorker"],
Expand Down

0 comments on commit 036e307

Please sign in to comment.