ssb-keyring
Version:
A persistence store for encryption keys for scuttlebutt. It's purpose is to make easy to answer box2 encryption/ decryption questions.
158 lines (129 loc) • 4.61 kB
JavaScript
const pull = require('pull-stream')
const { read } = require('pull-level')
const lFind = require('lodash.find')
const { isGroup, isSameKey, isBuffer } = require('../util')
const { processInfo } = require('./helpers')
const GROUP = 'group'
module.exports = function Group (db) {
let cache = new Map() // Map: groupId => group.info
function isExcludedGroup (groupId) {
const group = cache.get(groupId)
return !!group.excluded
}
return {
load (cb) {
readPersisted((err, pairs) => {
if (err) return cb(err)
cache = new Map(pairs)
cb(null)
})
},
add (groupId, addInfo, cb) {
if (!cache) throw new Error('keyring not ready')
if (!isGroup(groupId)) {
return cb(new Error(`expected a groupId, got ${groupId}`))
}
const groupInfo = cache.get(groupId) ?? {}
processInfo(groupInfo, addInfo, (err) => {
if (err) return cb(err)
// groupInfo gets mutated
cache.set(groupId, groupInfo)
db.put([GROUP, groupId], groupInfo, cb)
})
},
pickWriteKey (groupId, pickedKey, cb) {
if (!cache) throw new Error('keyring not ready')
if (!isGroup(groupId)) {
return cb(new Error(`expected a groupId, got ${groupId}`))
}
const groupInfo = cache.get(groupId)
if (!groupInfo) return cb(new Error("Couldn't find a group with ID " + groupId))
if (groupInfo.excluded) return cb(new Error('You are not in this group anymore. GroupId:' + groupId))
if (!pickedKey?.key || !isBuffer(pickedKey.key)) return cb(new Error('Missing key buffer in picked key'))
if (!pickedKey?.scheme) return cb(new Error('Missing scheme in picked key'))
if (isSameKey(groupInfo.writeKey.key, pickedKey.key) && groupInfo.writeKey.scheme === pickedKey.scheme) {
// if the key we're picking is already picked, return early
return cb()
}
const foundKey = lFind(
groupInfo.readKeys,
readKey => isSameKey(readKey.key, pickedKey.key) && readKey.scheme === pickedKey.scheme
)
if (!foundKey) return cb(new Error('Picked key does not match any current read key'))
groupInfo.writeKey = pickedKey
cache.set(groupId, groupInfo)
db.put([GROUP, groupId], groupInfo, cb)
},
has (groupId) {
if (!cache) throw new Error('keyring not ready')
return cache.has(groupId) && !isExcludedGroup(groupId)
},
get (groupId) {
if (!cache) throw new Error('keyring not ready')
return cache.get(groupId)
},
getUpdates (groupId) {
if (!cache) throw new Error('keyring not ready')
// [0] is id, [1] is info
return pull(
groupStream({ live: true }),
pull.filter((group) => group[0] === groupId),
pull.map((group) => group[1])
)
},
exclude (groupId, cb) {
if (!cache) throw new Error('keyring not ready')
if (!isGroup(groupId)) {
return cb(new Error(`expected a groupId, got ${groupId}`))
}
const groupInfo = cache.get(groupId)
if (!groupInfo) return cb(new Error("Couldn't find a group with ID " + groupId))
delete groupInfo.writeKey
groupInfo.excluded = true
cache.set(groupId, groupInfo)
db.put([GROUP, groupId], groupInfo, cb)
},
list (opts = {}) {
if (!cache) throw new Error('keyring not ready')
if (opts.live) {
return pull(
groupStream({ live: true }),
pull.map(group => group[0]), // only get id
// if opts.excluded == true then only return excluded groups, if false then only return not excluded groups
pull.filter(groupId => !!opts.excluded === isExcludedGroup(groupId))
)
} else {
return pull.values(this.listSync({ excluded: opts.excluded }))
}
},
listSync (opts = {}) {
if (!cache) throw new Error('keyring not ready')
return Array.from(cache.keys())
.filter(groupId => !!opts.excluded === isExcludedGroup(groupId))
}
}
function groupStream (opts = {}) {
return pull(
read(db, {
gt: [GROUP, null],
lt: [GROUP + '~', undefined],
live: !!opts.live
}),
pull.map(({ key, value: info, sync }) => {
if (sync) return null
const [_, groupId] = key; // eslint-disable-line
return [groupId, info]
}),
pull.filter(Boolean)
)
}
function readPersisted (cb) {
pull(
groupStream(),
pull.collect((err, pairs) => {
if (err) return cb(err)
cb(null, pairs)
})
)
}
}