diff --git a/README.md b/README.md index b141150..6154b1f 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. +- `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 + - `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)) + +- `hasPoBox(poBoxId, cb) => Boolean`: If a poBox with the given id is currently stored. Returns a promise if cb isn't provided. + +- `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 diff --git a/format.js b/format.js index c9885b2..13f835f 100644 --- a/format.js +++ b/format.js @@ -9,7 +9,7 @@ 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 { SecretKey, DHKeys, poBoxKey } = require('ssb-private-group-keys') const { keySchemes } = require('private-group-spec') const Keyring = require('ssb-keyring') const { ReadyGate } = require('./utils') @@ -61,8 +61,12 @@ function makeEncryptionFormat() { ) } - function isGroupId(recp) { - return keyring.group.has(recp) + function isGroupId(id) { + return Ref.isCloakedMsgId(id) || Uri.isIdentityGroupSSBURI(id) + } + + function isPoBoxId(id) { + return Uri.isIdentityPOBoxSSBURI(id) } function isFeed(recp) { @@ -185,6 +189,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) @@ -216,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)) { @@ -226,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) }) @@ -286,28 +336,63 @@ 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) 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() - const selfKey = selfDecryptionKeys(authorId) - const dmKey = dmDecryptionKeys(authorId) - - const unboxWith = unbox.bind(null, ciphertextBuf, authorBFE, previousBFE) - - let plaintextBuf = null - 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)) + if ((plaintextBuf = unboxWith(poBoxKeys, ATTEMPT16))) return plaintextBuf + return null } @@ -327,6 +412,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/index.js b/test/index.js index 8b789db..c8189f0 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,43 @@ test('encrypt accepts keys as recps', (t) => { t.end() }) }) + +test('decrypt as pobox recipient', (t) => { + const box2 = Box2() + const keys = ssbKeys.generate(null, 'alice', 'classic') + + 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, + }, (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 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 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() +})