ssb-keyring
Version:
A persistence store for encryption keys for scuttlebutt. It's purpose is to make easy to answer box2 encryption/ decryption questions.
369 lines (297 loc) • 10.7 kB
JavaScript
const test = require('tape')
const { keySchemes } = require('private-group-spec')
const { promisify: p } = require('util')
const na = require('sodium-universal')
const pull = require('pull-stream')
const ssbKeys = require('ssb-keys')
const Keyring = require('../')
// specifically 1.1.0, the last one ahau uses before the keyring format migration
const OldKeyring = require('ssb-keyring')
const { tmpPath, GroupKey, GroupId, GroupURIId, MsgId } = require('./helpers')
test('keyring.group', async t => {
const path = tmpPath()
let keyring = await Keyring(path)
let DESCRIPTION
/* keys.group.add(groupId, keyInfo, cb) */
DESCRIPTION = 'group.add, bad groupId => error'
await p(keyring.group.add)('junk', { key: GroupKey(), root: MsgId() })
.then(res => t.fail(DESCRIPTION))
.catch(err => t.match(err && err.message, /expected a groupId/, DESCRIPTION)) // ✓
DESCRIPTION = 'group.add, bad info.key => error'
await p(keyring.group.add)(GroupId(), { key: 'junk', root: MsgId() })
.then(res => t.fail(DESCRIPTION))
.catch(err => t.match(err && err.message, /expected buffer of length 32/, DESCRIPTION)) // ✓
DESCRIPTION = 'group.add, bad info.root => error'
await p(keyring.group.add)(GroupId(), { key: GroupKey(), root: 'dog' })
.then(res => t.fail(DESCRIPTION))
.catch(err => t.match(err && err.message, /expected info.root to be MsgId/, DESCRIPTION)) // ✓
const groupId = GroupId()
const addInfo = { key: GroupKey(), root: MsgId() }
DESCRIPTION = 'info.key must be na.crypto_secretbox_KEYBYTES long'
t.equals(addInfo.key.length, na.crypto_secretbox_KEYBYTES, DESCRIPTION)
DESCRIPTION = 'group.add, works!'
await p(keyring.group.add)(groupId, addInfo)
.then(() => t.pass(DESCRIPTION)) // ✓
.catch((err) => err && t.fail(err, DESCRIPTION))
/* keys.group.get(groupId) */
DESCRIPTION = 'group.get'
const expectedKey = { key: addInfo.key, scheme: keySchemes.private_group }
const expected = {
writeKey: expectedKey,
readKeys: [expectedKey],
root: addInfo.root
}
t.deepEqual(keyring.group.get(groupId), expected, DESCRIPTION)
t.deepEqual(keyring.group.get(GroupId()), undefined, DESCRIPTION + ' (unknown groupId)')
DESCRIPTION = '...persists'
await keyring.close()
keyring = await Keyring(path)
t.deepEqual(keyring.group.get(groupId), expected, DESCRIPTION)
/* keys.group.has(groupId) */
DESCRIPTION = 'group.has'
t.true(keyring.group.has(groupId), DESCRIPTION)
t.false(keyring.group.has(GroupId()), DESCRIPTION + ' (unknown groupId)')
/* keys.group.listSync() */
DESCRIPTION = 'group.listSync'
t.deepEqual(keyring.group.listSync(), [groupId], DESCRIPTION)
/* keys.group.list() */
DESCRIPTION = 'group.list'
t.deepEqual(
await pull(keyring.group.list(), pull.collectAsPromise()),
[groupId],
DESCRIPTION
)
const groupIds = []
pull(
keyring.group.list({ live: true }),
pull.drain((groupId) => {
groupIds.push(groupId)
}, (err) => {
if (err) t.fail(err)
})
)
await p(setTimeout)(500)
t.deepEqual(
groupIds,
[groupId],
DESCRIPTION + ' live'
)
DESCRIPTION = 'group.add second time works'
const groupId2 = GroupId()
const addInfo2 = { key: GroupKey(), root: MsgId() }
await p(keyring.group.add)(groupId2, addInfo2)
.then(() => t.pass(DESCRIPTION)) // ✓
.catch((err) => err && t.fail(err, DESCRIPTION))
await p(setTimeout)(500)
t.deepEqual(
groupIds,
[groupId, groupId2],
'group.list live is actually live'
)
await p(keyring.group.pickWriteKey)(groupId2, { key: addInfo2.key, scheme: keySchemes.private_group })
.catch((err) => t.fail(err))
await p(setTimeout)(500)
t.deepEqual(
groupIds,
[groupId, groupId2],
"group.pickWriteKey picking already picked key doesn't emit group update"
)
keyring.close()
t.end()
})
test('keyring.group URI', async t => {
const path = tmpPath()
const keyring = await Keyring(path)
const DESCRIPTION = 'group.add, accepts URI as root'
await p(keyring.group.add)(GroupId(), { key: GroupKey(), root: 'ssb:message/buttwoo-v1/59QdMLwxJzm7WHrNjxMyN06UPmaKhvId5KO4P2XjKhg=' })
.then(() => t.pass(DESCRIPTION))
.catch(err => { console.error(err); t.fail(DESCRIPTION) })
keyring.close()
t.end()
})
test('accepts ssb:identity/group URIS', async t => {
const path = tmpPath()
const keyring = await Keyring(path)
const DESCRIPTION = 'group.add, accepts URI as groupId'
await p(keyring.group.add)(GroupURIId(), { key: GroupKey(), root: 'ssb:message/buttwoo-v1/59QdMLwxJzm7WHrNjxMyN06UPmaKhvId5KO4P2XjKhg=' })
.then(() => t.pass(DESCRIPTION))
.catch(err => { console.error(err); t.fail(DESCRIPTION) })
keyring.close()
t.end()
})
test('Can store multiple read keys and pick between them for writing', async t => {
const path = tmpPath()
const keyring = await Keyring(path)
const groupId = GroupURIId()
const root = MsgId()
const key1 = {
key: GroupKey(),
scheme: keySchemes.private_group
}
const key2 = {
key: GroupKey(),
scheme: keySchemes.po_box
}
let DESCRIPTION = 'Adding the first key works (without scheme)'
await p(keyring.group.add)(groupId, { key: key1.key, root })
.then(() => t.pass(DESCRIPTION))
.catch(err => { console.error(err); t.fail(DESCRIPTION) })
t.deepEquals(keyring.group.get(groupId), {
writeKey: key1,
readKeys: [key1],
root
}, 'Getting the first key works')
DESCRIPTION = 'Adding the second key works'
await p(keyring.group.add)(groupId, { ...key2 })
.then(() => t.pass(DESCRIPTION))
.catch(err => { console.error(err); t.fail(DESCRIPTION) })
t.deepEquals(keyring.group.get(groupId), {
writeKey: key1,
readKeys: [key1, key2],
root
}, 'Getting the second key works')
DESCRIPTION = 'Picking a different write key works'
await p(keyring.group.pickWriteKey)(groupId, key2)
.then(() => t.pass(DESCRIPTION))
.catch(err => { console.error(err); t.fail(DESCRIPTION) })
t.deepEquals(keyring.group.get(groupId), {
writeKey: key2,
readKeys: [key1, key2],
root
}, 'Getting the newly picked key works')
keyring.close()
t.end()
})
test('can be excluded from a group', async t => {
const path = tmpPath()
const keyring = await Keyring(path)
const groupId = GroupURIId()
const key = GroupKey()
const root = MsgId()
const DESCRIPTION = 'Adding the group works'
await p(keyring.group.add)(groupId, { key, root })
.then(() => t.pass(DESCRIPTION))
.catch(err => { console.error(err); t.fail(DESCRIPTION) })
t.false(!!keyring.group.get(groupId).excluded, 'Not excluded from the group yet')
await p(keyring.group.exclude)(groupId)
t.deepEquals(
keyring.group.get(groupId),
{
readKeys: [{ key, scheme: keySchemes.private_group }],
root,
excluded: true
},
'Been excluded from the group'
)
const listExcluded = await pull(
keyring.group.list({ excluded: true }),
pull.collectAsPromise()
)
t.deepEquals(listExcluded, [groupId], 'able to get group from list with excluded opt')
const listNotExcluded = await pull(
keyring.group.list(),
pull.collectAsPromise()
)
t.deepEquals(listNotExcluded, [], 'getting no groups when listing not excluded groups')
t.deepEquals(keyring.group.listSync({ excluded: true }), [groupId], 'able to get group from listSync with excluded opt')
t.deepEquals(keyring.group.listSync(), [], 'getting no groups when listSyncing not excluded groups')
const key2 = GroupKey()
await p(keyring.group.add)(groupId, { key: key2 })
t.deepEquals(keyring.group.listSync(), [groupId], 'adding a new key un-excluded us')
await p(keyring.group.exclude)(groupId)
t.deepEquals(keyring.group.listSync(), [], 'excluded again')
await p(keyring.group.add)(groupId, { key: key2 })
t.deepEquals(keyring.group.listSync(), [groupId], 'adding an old key un-excluded us')
t.deepEquals(
keyring.group.get(groupId),
{
writeKey: { key: key2, scheme: keySchemes.private_group },
readKeys: [
{ key, scheme: keySchemes.private_group },
{ key: key2, scheme: keySchemes.private_group }
],
root
},
'correct group info format after un-exclusion'
)
})
test('getUpdates works', async t => {
const path = tmpPath()
const keyring = await Keyring(path)
const groupId = GroupURIId()
const key1 = GroupKey()
const key2 = GroupKey()
const root = MsgId()
await p(keyring.group.add)(groupId, { key: key1, root })
const groupUpdates = []
pull(
keyring.group.getUpdates(groupId),
pull.drain((update) => {
groupUpdates.push(update)
}, (err) => {
if (err) t.fail(err)
})
)
await p(setTimeout)(500)
t.equals(groupUpdates.length, 1, 'only initial info so far')
t.equals(
groupUpdates[0].writeKey.key.toString('base64'),
key1.toString('base64'),
'writeKey is the one we added'
)
await p(keyring.group.add)(groupId, { key: key2, root })
await p(setTimeout)(500)
t.equals(groupUpdates.length, 2, 'got new key info')
t.equals(
groupUpdates[1].readKeys.length,
2,
'there are two readKeys now'
)
await p(keyring.group.pickWriteKey)(groupId, { key: key2, scheme: keySchemes.private_group })
await p(setTimeout)(500)
t.equals(groupUpdates.length, 3, 'got info about new key pick')
t.equals(
groupUpdates[2].writeKey.key.toString('base64'),
key2.toString('base64'),
'the new writeKey is the one we switched to'
)
await p(keyring.group.exclude)(groupId)
await p(setTimeout)(500)
t.deepEquals(groupUpdates[3].excluded, true, 'got live update about exclusion')
const excludedUpdate = await pull(
keyring.group.getUpdates(groupId),
pull.take(1),
pull.collectAsPromise()
)
t.equals(excludedUpdate[0].excluded, true, 'got old update about exclusion')
})
test('migrate from old db format works', async t => {
const path = tmpPath()
const groupId = GroupId()
const key = GroupKey()
const groupInfo = {
key,
scheme: keySchemes.private_group
}
const root = MsgId()
const feedKeys = ssbKeys.generate()
const oldKeyring = await p(OldKeyring)(path, feedKeys)
await p(oldKeyring.group.register)(groupId, { key, root })
t.deepEqual(oldKeyring.group.get(groupId),
{
...groupInfo,
root
},
'group stored in old format'
)
await p(oldKeyring.close)()
const keyring = await Keyring(path)
t.deepEqual(keyring.group.get(groupId),
{
writeKey: groupInfo,
readKeys: [groupInfo],
root
},
'group stored in old format has gotten converted'
)
})