Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add poBox functions #34

Merged
merged 8 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
- `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<poBoxId>`: A pull stream of all the currently stored poBox ids.
Powersource marked this conversation as resolved.
Show resolved Hide resolved

## DM Encryption

Expand Down
89 changes: 88 additions & 1 deletion 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 @@ -65,6 +65,10 @@ function makeEncryptionFormat() {
return keyring.group.has(recp)
}

function isPoBoxId(recp) {
mixmix marked this conversation as resolved.
Show resolved Hide resolved
return keyring.poBox.has(recp)
}

function isFeed(recp) {
return (
Ref.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,6 +336,35 @@ function makeEncryptionFormat() {
}
}

function poBoxDecryptionKey(authorId, authorIdBFE, poBoxId) {
// TODO - consider how to reduce redundent computation + memory use here
mixmix marked this conversation as resolved.
Show resolved Hide resolved
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)
Expand All @@ -299,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)

Expand All @@ -307,6 +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
if ((plaintextBuf = unboxWith(poBoxKeys, ATTEMPT16))) return plaintextBuf
mixmix marked this conversation as resolved.
Show resolved Hide resolved

return null
}
Expand All @@ -327,6 +410,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()
})