Skip to content

Commit

Permalink
Merge pull request #106 from ssbc/added-late
Browse files Browse the repository at this point in the history
People added late to a group should get access to all epochs
  • Loading branch information
Powersource committed May 18, 2023
2 parents 25d5c0f + cadcbe7 commit ae0d64a
Show file tree
Hide file tree
Showing 17 changed files with 588 additions and 185 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,11 @@ NOTE: If `create` finds an empty (i.e. seemingly unused) group feed, it will sta

- `id` _GroupUri_ - an SSB URI that's safe to use publicly to name the group, and is used in `recps` to trigger encrypting messages to that group
- `subfeed` _Keys_ - the keys of the subfeed you should publish group data to
- `writeKey` _GroupKey_ - the current key used for publishing new messages to the group. It is one of the `readKeys`.
- `readKeys` _[GroupKey]_ - an array of all keys used to read messages for this group.
- `writeKey` _GroupSecret_ - the current key used for publishing new messages to the group. It is one of the `readKeys`.
- `readKeys` _[GroupSecret]_ - an array of all keys used to read messages for this group.
- `root` _MessagedId_ - the MessageId of the `group/init` message of the group, encoded as an ssb-uri.

where _GroupKey_ is an object of the format
where _GroupSecret_ is an object of the format

- `key` _Buffer_ - the symmetric key used by the group for encryption
- `scheme` _String_ - the scheme for this key
Expand All @@ -139,7 +139,7 @@ Publish `group/add-member` messages to a group of peers, which gives them all th
- `feedIds` _[FeedId]_ - an Array of 1-15 different ids for peers (accepts ssb-uri or sigil feed ids)
- `opts` _Object_ - with the options:
- `text` _String_ - A piece of text attached to the addition. Visible to the whole group and the newly added people.
- `cb` _Function_ - a callback of signature `(err, msg)`
- `cb` _Function_ - a callback of signature `(err, Array<msg>)`

### `ssb.tribes2.excludeMembers(groupId, feedIds, opts, cb)

Expand Down
129 changes: 90 additions & 39 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ const clarify = require('clarify-error')
const {
where,
and,
or,
isDecrypted,
type,
toPullStream,
Expand Down Expand Up @@ -64,7 +63,12 @@ module.exports = {
findOrCreateGroupWithoutMembers,
getRootFeedIdFromMsgId,
} = MetaFeedHelpers(ssb)
const { getPreferredEpoch, getMembers } = Epochs(ssb)
const {
getTipEpochs,
getPredecessorEpochs,
getPreferredEpoch,
getMembers,
} = Epochs(ssb)

function create(opts = {}, cb) {
if (cb === undefined) return promisify(create)(opts)
Expand All @@ -80,7 +84,7 @@ module.exports = {
const data = {
id: buildGroupId({
groupInitMsg,
groupKey: secret.toBuffer(),
secret: secret.toBuffer(),
}),
writeKey: {
key: secret.toBuffer(),
Expand Down Expand Up @@ -136,6 +140,8 @@ module.exports = {
function addMembers(groupId, feedIds, opts = {}, cb) {
if (cb === undefined) return promisify(addMembers)(groupId, feedIds, opts)

opts.oldSecrets ??= true

if (!feedIds || feedIds.length === 0) {
return cb(new Error('No feedIds provided to addMembers'))
}
Expand All @@ -147,42 +153,66 @@ module.exports = {
return cb(new Error('addMembers only supports bendybutt-v1 feed IDs'))
}

get(groupId, (err, { writeKey, root }) => {
get(groupId, (err, { root }) => {
// prettier-ignore
if (err) return cb(clarify(err, `Failed to get group details when adding members`))

getRootFeedIdFromMsgId(root, (err, rootAuthorId) => {
// prettier-ignore
if (err) return cb(clarify(err, "couldn't get root id of author of root msg"))

const content = {
type: 'group/add-member',
version: 'v2',
groupKey: writeKey.key.toString('base64'),
root,
creator: rootAuthorId,
recps: [groupId, ...feedIds],
}
getTipEpochs(groupId, (err, tipEpochs) => {
// prettier-ignore
if (err) return cb(clarify(err, "Couldn't get tip epochs when adding members"))

if (opts.text) content.text = opts.text
const getFeed = opts?._feedKeys
? (cb) => cb(null, { keys: opts._feedKeys })
: findOrCreateAdditionsFeed

const getFeed = opts?._feedKeys
? (cb) => cb(null, { keys: opts._feedKeys })
: findOrCreateAdditionsFeed
getFeed((err, additionsFeed) => {
// prettier-ignore
if (err) return cb(clarify(err, 'Failed to find or create additions feed when adding members'))

getFeed((err, additionsFeed) => {
// prettier-ignore
if (err) return cb(clarify(err, 'Failed to find or create additions feed when adding members'))
const options = {
isValid: isAddMember,
tangles: ['members'],
feedKeys: additionsFeed.keys,
}
pull(
pull.values(tipEpochs),
pull.asyncMap((tipEpoch, cb) => {
getPredecessorEpochs(
groupId,
tipEpoch.id,
(err, predecessors) => {
// prettier-ignore
if (err) return cb(clarify(err, "Couldn't get predecessor epochs when adding members"))

const oldSecrets = predecessors.map((pred) =>
pred.secret.toString('base64')
)
const content = {
type: 'group/add-member',
version: 'v2',
secret: tipEpoch.secret.toString('base64'),
oldSecrets: opts.oldSecrets ? oldSecrets : undefined,
root,
creator: rootAuthorId,
text: opts?.text,
recps: [groupId, ...feedIds],
}
return cb(null, content)
}
)
}),
pull.asyncMap((content, cb) => publish(content, options, cb)),
pull.collect((err, msgs) => {
// prettier-ignore
if (err) return cb(clarify(err, 'Failed to publish add-member message(s)'))

const options = {
isValid: isAddMember,
tangles: ['members'],
feedKeys: additionsFeed.keys,
}
publish(content, options, (err, msg) => {
// prettier-ignore
if (err) return cb(clarify(err, 'Failed to publish add-member message'))
return cb(null, msg)
return cb(null, msgs)
})
)
})
})
})
Expand Down Expand Up @@ -221,15 +251,15 @@ module.exports = {
const remainingMembers = beforeMembers.filter(
(member) => !feedIds.includes(member)
)
const newGroupKey = new SecretKey()
const addInfo = { key: newGroupKey.toBuffer() }
const newSecret = new SecretKey()
const addInfo = { key: newSecret.toBuffer() }

ssb.box2.addGroupInfo(groupId, addInfo, (err) => {
// prettier-ignore
if (err) return cb(clarify(err, "Couldn't store new key when excluding members"))

const newKey = {
key: newGroupKey.toBuffer(),
key: newSecret.toBuffer(),
scheme: keySchemes.private_group,
}
ssb.box2.pickGroupWriteKey(groupId, newKey, (err) => {
Expand All @@ -239,7 +269,7 @@ module.exports = {
const newEpochContent = {
type: 'group/init',
version: 'v2',
groupKey: newGroupKey.toString('base64'),
secret: newSecret.toString('base64'),
tangles: {
members: { root: null, previous: null },
},
Expand All @@ -256,7 +286,12 @@ module.exports = {
pull(
pull.values(chunk(remainingMembers, 15)),
pull.asyncMap((membersToAdd, cb) =>
addMembers(groupId, membersToAdd, {}, cb)
addMembers(
groupId,
membersToAdd,
{ oldSecrets: false },
cb
)
),
pull.collect((err) => {
// prettier-ignore
Expand Down Expand Up @@ -420,6 +455,7 @@ module.exports = {
function getGroupInviteData(groupId, cb) {
let root
const secrets = new Set()
let writeSecret = null

pull(
ssb.db.query(
Expand All @@ -436,7 +472,13 @@ module.exports = {
pull.drain(
(msg) => {
root ||= msg.value.content.root
secrets.add(msg.value.content.groupKey)
const oldSecrets = msg.value.content.oldSecrets
if (oldSecrets) {
oldSecrets.forEach((oldSecret) => secrets.add(oldSecret))
}
const latestSecret = msg.value.content.secret
secrets.add(latestSecret)
writeSecret = latestSecret
},
(err) => {
if (err) return cb(err)
Expand All @@ -449,6 +491,10 @@ module.exports = {
id: groupId,
root,
readKeys,
writeKey: {
key: Buffer.from(writeSecret, 'base64'),
scheme: keySchemes.private_group,
},
}
return cb(null, invite)
}
Expand All @@ -471,8 +517,8 @@ module.exports = {
if (!inviteInfos.length) return cb(new Error("Didn't find invite for that group id"))

// TODO: which writeKey should be picked??
// this will essentially pick a random write key
const { id, root, readKeys } = inviteInfos[0]
// this will essentially pick a random write key from the current epoch tips
const { id, root, readKeys, writeKey } = inviteInfos[0]
pull(
pull.values(readKeys),
pull.asyncMap((readKey, cb) => {
Expand All @@ -481,10 +527,15 @@ module.exports = {
pull.collect((err) => {
// prettier-ignore
if (err) return cb(clarify(err, 'Failed to add group info when accepting an invite'))
ssb.db.reindexEncrypted((err) => {
ssb.box2.pickGroupWriteKey(id, writeKey, (err) => {
// prettier-ignore
if (err) cb(clarify(err, 'Failed to reindex encrypted messages when accepting an invite'))
else cb(null, inviteInfos[0])
if (err) return cb(clarify(err, 'Failed to pick a write key when accepting invite to a group'))

ssb.db.reindexEncrypted((err) => {
// prettier-ignore
if (err) cb(clarify(err, 'Failed to reindex encrypted messages when accepting an invite'))
else cb(null, inviteInfos[0])
})
})
})
)
Expand Down
10 changes: 5 additions & 5 deletions lib/build-group-id.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module.exports = function buildGroupId({
groupInitMsg,
readKey,
msgKey,
groupKey,
secret,
}) {
const msgId = bfe.encode(groupInitMsg.key)

Expand All @@ -25,10 +25,10 @@ module.exports = function buildGroupId({
readKey = toBuffer(groupInitMsg.value.meta.unbox)
} else if (msgKey) {
readKey = fromMsgKey(groupInitMsg, msgKey)
} else if (groupKey) {
} else if (secret) {
// when we've just heard a group/add-member message, we need to calculate
// groupId and only have these two things
readKey = fromGroupKey(groupInitMsg, groupKey)
readKey = fromSecret(groupInitMsg, secret)
} else {
throw new Error('Read key must be defined???')
}
Expand All @@ -52,15 +52,15 @@ function fromMsgKey(msg, msgKey) {
return derive(msgKey, [LABELS.read_key])
}

function fromGroupKey(msg, groupKey) {
function fromSecret(msg, secret) {
const { author, previous, content } = msg.value

const envelope = Buffer.from(content.replace('.box2', ''), 'base64')
const feed_id = bfe.encode(author)
const prev_msg_id = bfe.encode(previous)

const group_key = {
key: toBuffer(groupKey),
key: toBuffer(secret),
scheme: keySchemes.private_group,
}
return unboxKey(envelope, feed_id, prev_msg_id, [group_key], {
Expand Down
54 changes: 52 additions & 2 deletions lib/epochs.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ function Epochs(ssb) {
ssb.metafeeds.findRootFeedId(epochRoot.value.author, cb)
},
secret(epochRoot, cb) {
cb(null, epochRoot.value.content.groupKey)
cb(null, epochRoot.value.content.secret)
},
members(epochRoot, cb) {
getMembers(epochRoot.key, cb)
Expand Down Expand Up @@ -105,6 +105,55 @@ function Epochs(ssb) {
})
}

function getTipEpochs(groupId, cb) {
if (cb === undefined) return p(this.getTipEpochs).call(this, groupId)

const opts = { getters: pluck(allGetters, ['author', 'secret']) }
epochsReduce(groupId, opts, (err, reduce) => {
// prettier-ignore
if (err) return cb(clarify(err, 'Failed to resolve epoch @tangle/reduce'))

const tips = Object.keys(reduce.state).map((id) => {
const info = {
id,
previous: reduce.graph.getNode(id).previous,
...reduce.state[id][id],
}
info.secret = Buffer.from(info.secret, 'base64')
return info
})

return cb(null, tips)
})
}

/** Gets all epochs leading up to the specified epoch, all the way back to the root.
* Does not include the specified epoch nor epochs in epoch forks.
*/
function getPredecessorEpochs(groupId, epochRootId, cb) {
if (cb === undefined)
return p(this.getPredecessorEpochs).call(this, groupId, epochRootId)

const opts = { getters: pluck(allGetters, ['author', 'secret']) }
epochsReduce(groupId, opts, (err, reduce) => {
// prettier-ignore
if (err) return cb(clarify(err, "Couldn't get epoch reducer when getting predecessors"))

const predecessors = reduce.graph.getHistory(epochRootId).map((id) => {
const node = reduce.graph.getNode(id)
const data = node.data[id]

return {
id,
previous: node.previous,
secret: Buffer.from(data.secret, 'base64'),
}
})

return cb(null, predecessors)
})
}

function getMembers(epochRootId, cb) {
if (cb === undefined) return p(getMembers)(epochRootId)

Expand Down Expand Up @@ -344,7 +393,6 @@ function Epochs(ssb) {
pull(
epochNodeStream(ssb, groupId, { getters, live }),
pull.map((node) => {
console.log('adding epoch', node.id)
reduce.addNodes([node])
return true
})
Expand Down Expand Up @@ -384,6 +432,8 @@ function Epochs(ssb) {

return {
getEpochs,
getTipEpochs,
getPredecessorEpochs,
getMembers,
getPreferredEpoch,
getMissingMembers,
Expand Down
2 changes: 1 addition & 1 deletion lib/meta-feed-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ module.exports = (ssb) => {
const content = {
type: 'group/init',
version: 'v2',
groupKey: secret.toString('base64'),
secret: secret.toString('base64'),
tangles: {
group: { root: null, previous: null },
epoch: { root: null, previous: null },
Expand Down
Loading

0 comments on commit ae0d64a

Please sign in to comment.