From eedd509c158e741541054db975c83ec123a3965f Mon Sep 17 00:00:00 2001 From: Jacob Karlsson Date: Tue, 17 Oct 2023 19:43:23 +0200 Subject: [PATCH 1/8] Add poBox functions --- format.js | 47 ++++++++++++++++++++++++++++ index.js | 4 +++ package.json | 2 +- test/pobox.js | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 test/pobox.js diff --git a/format.js b/format.js index c9885b2..f58762c 100644 --- a/format.js +++ b/format.js @@ -185,6 +185,49 @@ function makeEncryptionFormat() { return deferredSource } + function addPoBox(poBoxId, info, cb) { + if (cb === undefined) return promisify(addPoBox)(poBoxId, info) + + if (!poBoxId) cb(new Error('pobox id required')) + if (!info) cb(new Error('pobox info required')) + + keyringReady.onReady(() => { + keyring.poBox.add(poBoxId, info, cb) + }) + } + + function hasPoBox(poBoxId, cb) { + if (cb === undefined) return promisify(hasPoBox)(poBoxId) + + if (!poBoxId) cb(new Error('pobox id required')) + + keyringReady.onReady(() => { + cb(null, keyring.poBox.has(poBoxId)) + }) + } + + function getPoBox(poBoxId, cb) { + if (cb === undefined) return promisify(getPoBox)(poBoxId) + + if (!poBoxId) cb(new Error('pobox id required')) + + keyringReady.onReady(() => { + cb(null, keyring.poBox.get(poBoxId)) + }) + } + + function listPoBoxIds() { + const deferredSource = pullDefer.source() + + keyringReady.onReady(() => { + const source = pull.values(keyring.poBox.list()) + + deferredSource.resolve(source) + }) + + return deferredSource + } + function dmEncryptionKey(authorKeys, recp) { if (legacyMode) { if (!keyring.dm.has(authorKeys.id, recp)) addDMPairSync(authorKeys, recp) @@ -327,6 +370,10 @@ function makeEncryptionFormat() { getGroupInfo, getGroupInfoUpdates, canDM, + addPoBox, + hasPoBox, + getPoBox, + listPoBoxIds, // Internal APIs: addSigningKeys, addSigningKeysSync, diff --git a/index.js b/index.js index 91313ec..bc9d965 100644 --- a/index.js +++ b/index.js @@ -34,5 +34,9 @@ exports.init = function (ssb, config) { listGroupIds: encryptionFormat.listGroupIds, getGroupInfo: encryptionFormat.getGroupInfo, getGroupInfoUpdates: encryptionFormat.getGroupInfoUpdates, + addPoBox: encryptionFormat.addPoBox, + hasPoBox: encryptionFormat.hasPoBox, + getPoBox: encryptionFormat.getPoBox, + listPoBoxIds: encryptionFormat.listPoBoxIds, } } diff --git a/package.json b/package.json index 07ccc50..64d13d2 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "pull-defer": "^0.2.3", "pull-stream": "^3.6.14", "ssb-bfe": "^3.7.0", - "ssb-keyring": "^5.4.0", + "ssb-keyring": "^7.0.0", "ssb-private-group-keys": "^1.1.1", "ssb-ref": "^2.16.0", "ssb-uri2": "^2.4.1" diff --git a/test/pobox.js b/test/pobox.js new file mode 100644 index 0000000..0bf235d --- /dev/null +++ b/test/pobox.js @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2021 Anders Rune Jensen +// +// SPDX-License-Identifier: Unlicense + +const { promisify } = require('util') +const test = require('tape') +const ssbKeys = require('ssb-keys') +const path = require('path') +const rimraf = require('rimraf') +const mkdirp = require('mkdirp') +const SecretStack = require('secret-stack') +const caps = require('ssb-caps') +const bfe = require('ssb-bfe') +const { DHKeys } = require('ssb-private-group-keys') +const pull = require('pull-stream') +const { keySchemes } = require('private-group-spec') + +function readyDir(dir) { + rimraf.sync(dir) + mkdirp.sync(dir) + return dir +} + +const poBoxDH = new DHKeys().generate() + +const poBoxId = bfe.decode( + Buffer.concat([bfe.toTF('identity', 'po-box'), poBoxDH.toBuffer().public]) +) +const testkey = poBoxDH.toBuffer().secret + +let sbot +let keys + +function setup() { + const dir = readyDir('/tmp/ssb-db2-box2-tribes') + keys = ssbKeys.loadOrCreateSync(path.join(dir, 'secret')) + + sbot = SecretStack({ appKey: caps.shs }) + .use(require('ssb-db2/core')) + .use(require('ssb-classic')) + .use(require('ssb-db2/compat/publish')) + .use(require('ssb-db2/compat/post')) + .use(require('../')) + .call(null, { + keys, + path: dir, + box2: { + legacyMode: true, + }, + }) +} + +function tearDown(cb) { + if (cb === undefined) return promisify(tearDown)() + + sbot.close(true, cb) +} + +test('pobox functions', async (t) => { + setup() + + await sbot.box2.addPoBox(poBoxId, { key: testkey, scheme: keySchemes.po_box }) + + const has = await sbot.box2.hasPoBox(poBoxId) + + t.equal(has, true, 'we have the pobox stored now') + + const poBoxInfo = await sbot.box2.getPoBox(poBoxId) + + t.deepEquals( + poBoxInfo, + { + key: testkey, + scheme: keySchemes.po_box, + }, + 'can get pobox info' + ) + + const listPoBoxIds = await pull( + sbot.box2.listPoBoxIds(), + pull.collectAsPromise() + ) + t.deepEquals(listPoBoxIds, [poBoxId], 'can list the pobox') + + await tearDown() +}) From 41f0ee837b42afcaeb2d3b43cc777bd9b5f109ae Mon Sep 17 00:00:00 2001 From: Jacob Karlsson Date: Tue, 17 Oct 2023 19:51:37 +0200 Subject: [PATCH 2/8] PoBox readme --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index b141150..81fb380 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,18 @@ on the `sbot.box2` namespace: - `getGroupInfoUpdates(groupId) => PullStream`: Like `getGroupInfo` but instead returns a live pull stream that outputs the group info and then any time the group info is updated. - `canDM(myLeafFeedId, theirRootFeedId, cb)`: Checks if you can create an encrypted message ("DM") for a given `theirRootFeedId` (which must be a bendybutt-v1 root metafeed ID) using your `myLeafFeedId` as the author. Delivers a boolean on the callback. +- `keyring.poBox.add(poBoxId, info, cb)`: Stores the key to a poBox. Returns a promise if cb isn't provided. + + where + - `poBoxId` *String* is an SSB-URI for a P.O. Box + - `info` *Object* + - `info.key` *Buffer* - the private part of a diffie-hellman key + - `info.scheme` *String* the scheme associated with that key (currently optional (undefined by default)) + +- `keyring.poBox.has(poBoxId, cb) => Boolean`: If a poBox with the given id is currently stored. Returns a promise if cb isn't provided. + +- `keyring.poBox.get(poBoxId, cb) => keyInfo`: Gets a poBox's key info if stored. An object with a `key` buffer and a `scheme` if a scheme was stored. Returns a promise if cb isn't provided. +- `keyring.poBox.list(poBoxId) => PullStream`: A pull stream of all the currently stored poBox ids. ## DM Encryption From 45ca62401cdbd3b807498e5e2004fb0eccd740da Mon Sep 17 00:00:00 2001 From: Jacob Karlsson Date: Tue, 17 Oct 2023 20:09:52 +0200 Subject: [PATCH 3/8] Add failing enc to pobox test --- test/index.js | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/index.js b/test/index.js index 8b789db..c744cef 100644 --- a/test/index.js +++ b/test/index.js @@ -7,6 +7,8 @@ const { check } = require('ssb-encryption-format') const ssbKeys = require('ssb-keys') const buttwoo = require('ssb-buttwoo/format') const { keySchemes } = require('private-group-spec') +const { DHKeys } = require('ssb-private-group-keys') +const bfe = require('ssb-bfe') const Box2 = require('../format') @@ -352,3 +354,41 @@ test('encrypt accepts keys as recps', (t) => { t.end() }) }) + +test('decrypt as pobox recipient', (t) => { + const box2 = Box2() + const keys = ssbKeys.generate(null, 'alice', 'buttwoo-v1') + + const poBoxDH = new DHKeys().generate() + + const poBoxId = bfe.decode( + Buffer.concat([bfe.toTF('identity', 'po-box'), poBoxDH.toBuffer().public]) + ) + const testkey = poBoxDH.toBuffer().secret + + box2.setup({ keys }, () => { + box2.addPoBox(poBoxId, { + key: testkey, + }) + + const opts = { + keys, + content: { type: 'post', text: 'super secret' }, + previous: null, + timestamp: 12345678900, + tag: buttwoo.tags.SSB_FEED, + hmacKey: null, + recps: [poBoxId, ssbKeys.generate(null, '2').id], + } + + const plaintext = buttwoo.toPlaintextBuffer(opts) + t.true(Buffer.isBuffer(plaintext), 'plaintext is a buffer') + + const ciphertext = box2.encrypt(plaintext, opts) + + const decrypted = box2.decrypt(ciphertext, { ...opts, author: keys.id }) + t.deepEqual(decrypted, plaintext, 'decrypted plaintext is the same') + + t.end() + }) +}) \ No newline at end of file From 42ce713d90b75e73a43dd5600e9fe5c80862b724 Mon Sep 17 00:00:00 2001 From: Jacob Karlsson Date: Tue, 17 Oct 2023 21:23:44 +0200 Subject: [PATCH 4/8] Allow encryption/decryption of poboxes --- format.js | 61 +++++++++++++++++++++++++++++++++++++++++++++++++-- test/index.js | 36 ++++++++++++++++-------------- 2 files changed, 78 insertions(+), 19 deletions(-) diff --git a/format.js b/format.js index f58762c..201f91b 100644 --- a/format.js +++ b/format.js @@ -8,8 +8,8 @@ const Ref = require('ssb-ref') const Uri = require('ssb-uri2') const path = require('path') const os = require('os') -const { box, unbox } = require('envelope-js') -const { SecretKey, DHKeys } = require('ssb-private-group-keys') +const { box, unbox, unboxKey, unboxBody } = require('envelope-js') +const { SecretKey, DHKeys, poBoxKey } = require('ssb-private-group-keys') const { keySchemes } = require('private-group-spec') const Keyring = require('ssb-keyring') const { ReadyGate } = require('./utils') @@ -65,6 +65,10 @@ function makeEncryptionFormat() { return keyring.group.has(recp) } + function isPoBoxId(recp) { + return keyring.poBox.has(recp) + } + function isFeed(recp) { return ( Ref.isFeed(recp) || @@ -259,6 +263,7 @@ function makeEncryptionFormat() { const recps = opts.recps const authorId = opts.keys.id const previousId = opts.previous + const easyPoBoxKey = poBoxKey.easy(opts.keys) const encryptionKeys = recps.map((recp) => { if (isRawGroupKey(recp)) { @@ -269,6 +274,8 @@ function makeEncryptionFormat() { return dmEncryptionKey(opts.keys, recp) } else if (isGroupId(recp) && keyring.group.has(recp)) { return keyring.group.get(recp).writeKey + } else if (isPoBoxId(recp) && keyring.poBox.has(recp)) { + return easyPoBoxKey(recp) } else throw new Error('Unsupported recipient: ' + recp) }) @@ -351,6 +358,56 @@ function makeEncryptionFormat() { if ((plaintextBuf = unboxWith(selfKey, ATTEMPT16))) return plaintextBuf if ((plaintextBuf = unboxWith(dmKey, ATTEMPT16))) return plaintextBuf + /* check my poBox keys */ + // TODO - consider how to reduce redundent computation + memory use here + const trial_poBox_keys = keyring.poBox.list().map((poBoxId) => { + const data = keyring.poBox.get(poBoxId) + + const poBox_dh_secret = Buffer.concat([ + BFE.toTF('encryption-key', 'box2-pobox-dh'), + data.key, + ]) + + const poBox_id = BFE.encode(poBoxId) + const poBox_dh_public = Buffer.concat([ + BFE.toTF('encryption-key', 'box2-pobox-dh'), + poBox_id.slice(2), + ]) + + const author_dh_public = new DHKeys( + { public: authorId }, + { fromEd25519: true } + ).toBFE().public + + return poBoxKey( + poBox_dh_secret, + poBox_dh_public, + poBox_id, + author_dh_public, + authorBFE + ) + }) + + const tryPoBoxKey = unboxKey( + ciphertextBuf, + authorBFE, + previousBFE, + trial_poBox_keys, + { maxAttempts: 16 } + ) + if (tryPoBoxKey) { + if ( + (plaintextBuf = unboxBody( + ciphertextBuf, + authorBFE, + previousBFE, + tryPoBoxKey, + ATTEMPT16 + )) + ) + return plaintextBuf + } + return null } diff --git a/test/index.js b/test/index.js index c744cef..c8189f0 100644 --- a/test/index.js +++ b/test/index.js @@ -357,7 +357,7 @@ test('encrypt accepts keys as recps', (t) => { test('decrypt as pobox recipient', (t) => { const box2 = Box2() - const keys = ssbKeys.generate(null, 'alice', 'buttwoo-v1') + const keys = ssbKeys.generate(null, 'alice', 'classic') const poBoxDH = new DHKeys().generate() @@ -369,26 +369,28 @@ test('decrypt as pobox recipient', (t) => { box2.setup({ keys }, () => { box2.addPoBox(poBoxId, { key: testkey, - }) + }, (err) => { + t.error(err, "added pobox key") - const opts = { - keys, - content: { type: 'post', text: 'super secret' }, - previous: null, - timestamp: 12345678900, - tag: buttwoo.tags.SSB_FEED, - hmacKey: null, - recps: [poBoxId, ssbKeys.generate(null, '2').id], - } + const opts = { + keys, + content: { type: 'post', text: 'super secret' }, + previous: null, + timestamp: 12345678900, + tag: buttwoo.tags.SSB_FEED, + hmacKey: null, + recps: [poBoxId, ssbKeys.generate(null, '2').id], + } - const plaintext = buttwoo.toPlaintextBuffer(opts) - t.true(Buffer.isBuffer(plaintext), 'plaintext is a buffer') + const plaintext = buttwoo.toPlaintextBuffer(opts) + t.true(Buffer.isBuffer(plaintext), 'plaintext is a buffer') - const ciphertext = box2.encrypt(plaintext, opts) + const ciphertext = box2.encrypt(plaintext, opts) - const decrypted = box2.decrypt(ciphertext, { ...opts, author: keys.id }) - t.deepEqual(decrypted, plaintext, 'decrypted plaintext is the same') + const decrypted = box2.decrypt(ciphertext, { ...opts, author: keys.id }) + t.deepEqual(decrypted, plaintext, 'decrypted plaintext is the same') - t.end() + t.end() + }) }) }) \ No newline at end of file From d20620cb010b2cc43bc73835aa8fd796b09807e5 Mon Sep 17 00:00:00 2001 From: Jacob Karlsson Date: Tue, 17 Oct 2023 21:29:41 +0200 Subject: [PATCH 5/8] Simplify pobox decryption --- format.js | 85 ++++++++++++++++++++++--------------------------------- 1 file changed, 34 insertions(+), 51 deletions(-) diff --git a/format.js b/format.js index 201f91b..0c46a09 100644 --- a/format.js +++ b/format.js @@ -8,7 +8,7 @@ const Ref = require('ssb-ref') const Uri = require('ssb-uri2') const path = require('path') const os = require('os') -const { box, unbox, unboxKey, unboxBody } = require('envelope-js') +const { box, unbox } = require('envelope-js') const { SecretKey, DHKeys, poBoxKey } = require('ssb-private-group-keys') const { keySchemes } = require('private-group-spec') const Keyring = require('ssb-keyring') @@ -336,6 +336,35 @@ function makeEncryptionFormat() { } } + function poBoxDecryptionKey(authorId, authorIdBFE, poBoxId) { + // TODO - consider how to reduce redundent computation + memory use here + const data = keyring.poBox.get(poBoxId) + + const poBox_dh_secret = Buffer.concat([ + BFE.toTF('encryption-key', 'box2-pobox-dh'), + data.key, + ]) + + const poBox_id = BFE.encode(poBoxId) + const poBox_dh_public = Buffer.concat([ + BFE.toTF('encryption-key', 'box2-pobox-dh'), + poBox_id.slice(2), + ]) + + const author_dh_public = new DHKeys( + { public: authorId }, + { fromEd25519: true } + ).toBFE().public + + return poBoxKey( + poBox_dh_secret, + poBox_dh_public, + poBox_id, + author_dh_public, + authorIdBFE + ) + } + function decrypt(ciphertextBuf, opts) { const authorId = opts.author const authorBFE = BFE.encode(authorId) @@ -349,6 +378,9 @@ function makeEncryptionFormat() { .flat() const selfKey = selfDecryptionKeys(authorId) const dmKey = dmDecryptionKeys(authorId) + const poBoxKeys = keyring.poBox + .list() + .map((poBoxId) => poBoxDecryptionKey(authorId, authorBFE, poBoxId)) const unboxWith = unbox.bind(null, ciphertextBuf, authorBFE, previousBFE) @@ -357,56 +389,7 @@ function makeEncryptionFormat() { if ((plaintextBuf = unboxWith(groupKeys, ATTEMPT1))) return plaintextBuf if ((plaintextBuf = unboxWith(selfKey, ATTEMPT16))) return plaintextBuf if ((plaintextBuf = unboxWith(dmKey, ATTEMPT16))) return plaintextBuf - - /* check my poBox keys */ - // TODO - consider how to reduce redundent computation + memory use here - const trial_poBox_keys = keyring.poBox.list().map((poBoxId) => { - const data = keyring.poBox.get(poBoxId) - - const poBox_dh_secret = Buffer.concat([ - BFE.toTF('encryption-key', 'box2-pobox-dh'), - data.key, - ]) - - const poBox_id = BFE.encode(poBoxId) - const poBox_dh_public = Buffer.concat([ - BFE.toTF('encryption-key', 'box2-pobox-dh'), - poBox_id.slice(2), - ]) - - const author_dh_public = new DHKeys( - { public: authorId }, - { fromEd25519: true } - ).toBFE().public - - return poBoxKey( - poBox_dh_secret, - poBox_dh_public, - poBox_id, - author_dh_public, - authorBFE - ) - }) - - const tryPoBoxKey = unboxKey( - ciphertextBuf, - authorBFE, - previousBFE, - trial_poBox_keys, - { maxAttempts: 16 } - ) - if (tryPoBoxKey) { - if ( - (plaintextBuf = unboxBody( - ciphertextBuf, - authorBFE, - previousBFE, - tryPoBoxKey, - ATTEMPT16 - )) - ) - return plaintextBuf - } + if ((plaintextBuf = unboxWith(poBoxKeys, ATTEMPT16))) return plaintextBuf return null } From c4de9f0572093c0bbc6e08568e3d87ca79ae903c Mon Sep 17 00:00:00 2001 From: Jacob Karlsson Date: Sun, 22 Oct 2023 16:14:50 +0200 Subject: [PATCH 6/8] Fix function names in readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 81fb380..6154b1f 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ on the `sbot.box2` namespace: - `getGroupInfoUpdates(groupId) => PullStream`: Like `getGroupInfo` but instead returns a live pull stream that outputs the group info and then any time the group info is updated. - `canDM(myLeafFeedId, theirRootFeedId, cb)`: Checks if you can create an encrypted message ("DM") for a given `theirRootFeedId` (which must be a bendybutt-v1 root metafeed ID) using your `myLeafFeedId` as the author. Delivers a boolean on the callback. -- `keyring.poBox.add(poBoxId, info, cb)`: Stores the key to a poBox. Returns a promise if cb isn't provided. +- `addPoBox(poBoxId, info, cb)`: Stores the key to a poBox. Returns a promise if cb isn't provided. where - `poBoxId` *String* is an SSB-URI for a P.O. Box @@ -95,10 +95,10 @@ on the `sbot.box2` namespace: - `info.key` *Buffer* - the private part of a diffie-hellman key - `info.scheme` *String* the scheme associated with that key (currently optional (undefined by default)) -- `keyring.poBox.has(poBoxId, cb) => Boolean`: If a poBox with the given id is currently stored. Returns a promise if cb isn't provided. +- `hasPoBox(poBoxId, cb) => Boolean`: If a poBox with the given id is currently stored. Returns a promise if cb isn't provided. -- `keyring.poBox.get(poBoxId, cb) => keyInfo`: Gets a poBox's key info if stored. An object with a `key` buffer and a `scheme` if a scheme was stored. Returns a promise if cb isn't provided. -- `keyring.poBox.list(poBoxId) => PullStream`: A pull stream of all the currently stored poBox ids. +- `getPoBox(poBoxId, cb) => keyInfo`: Gets a poBox's key info if stored. An object with a `key` buffer and a `scheme` if a scheme was stored. Returns a promise if cb isn't provided. +- `listPoBoxIds(poBoxId) => PullStream`: A pull stream of all the currently stored poBox ids. ## DM Encryption From 11cba55042e01f1d97429774756b7fe5efd618dc Mon Sep 17 00:00:00 2001 From: Jacob Karlsson Date: Tue, 24 Oct 2023 12:36:21 +0200 Subject: [PATCH 7/8] Only calculate poboxkeys if maybe needed --- format.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/format.js b/format.js index 0c46a09..a3ac6f3 100644 --- a/format.js +++ b/format.js @@ -370,25 +370,27 @@ function makeEncryptionFormat() { const authorBFE = BFE.encode(authorId) const previousBFE = BFE.encode(opts.previous) + const unboxWith = unbox.bind(null, ciphertextBuf, authorBFE, previousBFE) + + let plaintextBuf = null + const groups = keyring.group.listSync() const excludedGroups = keyring.group.listSync({ excluded: true }) const groupKeys = [...groups, ...excludedGroups] .map(keyring.group.get) .map((groupInfo) => groupInfo.readKeys) .flat() + if ((plaintextBuf = unboxWith(groupKeys, ATTEMPT1))) return plaintextBuf + const selfKey = selfDecryptionKeys(authorId) + if ((plaintextBuf = unboxWith(selfKey, ATTEMPT16))) return plaintextBuf + const dmKey = dmDecryptionKeys(authorId) + if ((plaintextBuf = unboxWith(dmKey, ATTEMPT16))) return plaintextBuf + const poBoxKeys = keyring.poBox .list() .map((poBoxId) => poBoxDecryptionKey(authorId, authorBFE, poBoxId)) - - const unboxWith = unbox.bind(null, ciphertextBuf, authorBFE, previousBFE) - - let plaintextBuf = null - - if ((plaintextBuf = unboxWith(groupKeys, ATTEMPT1))) return plaintextBuf - if ((plaintextBuf = unboxWith(selfKey, ATTEMPT16))) return plaintextBuf - if ((plaintextBuf = unboxWith(dmKey, ATTEMPT16))) return plaintextBuf if ((plaintextBuf = unboxWith(poBoxKeys, ATTEMPT16))) return plaintextBuf return null From 72d900a6f5b5fbcdfc2764c264a3bee35b0b52a4 Mon Sep 17 00:00:00 2001 From: Jacob Karlsson Date: Tue, 24 Oct 2023 12:45:27 +0200 Subject: [PATCH 8/8] More direct isId functions --- format.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/format.js b/format.js index a3ac6f3..13f835f 100644 --- a/format.js +++ b/format.js @@ -61,12 +61,12 @@ function makeEncryptionFormat() { ) } - function isGroupId(recp) { - return keyring.group.has(recp) + function isGroupId(id) { + return Ref.isCloakedMsgId(id) || Uri.isIdentityGroupSSBURI(id) } - function isPoBoxId(recp) { - return keyring.poBox.has(recp) + function isPoBoxId(id) { + return Uri.isIdentityPOBoxSSBURI(id) } function isFeed(recp) {