Skip to content

Commit

Permalink
Merge pull request #34 from ssbc/pobox
Browse files Browse the repository at this point in the history
Add poBox functions
  • Loading branch information
Powersource authored Oct 30, 2023
2 parents 6cef638 + 72d900a commit 4ea5668
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 11 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ on the `sbot.box2` namespace:

- `getGroupInfoUpdates(groupId) => PullStream<groupInfo>`: 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<poBoxId>`: A pull stream of all the currently stored poBox ids.

## DM Encryption

Expand Down
109 changes: 99 additions & 10 deletions format.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)) {
Expand All @@ -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)
})

Expand Down Expand Up @@ -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
}

Expand All @@ -327,6 +412,10 @@ function makeEncryptionFormat() {
getGroupInfo,
getGroupInfoUpdates,
canDM,
addPoBox,
hasPoBox,
getPoBox,
listPoBoxIds,
// Internal APIs:
addSigningKeys,
addSigningKeysSync,
Expand Down
4 changes: 4 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
42 changes: 42 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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()
})
})
})
86 changes: 86 additions & 0 deletions test/pobox.js
Original file line number Diff line number Diff line change
@@ -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()
})

0 comments on commit 4ea5668

Please sign in to comment.