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

People added late to a group should get access to all epochs #106

Merged
merged 34 commits into from
May 18, 2023
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4930b3e
Split out getTipEpochs
Powersource May 4, 2023
5172531
One add-member per epoch tip
Powersource May 4, 2023
50c94c1
Update create test content
Powersource May 4, 2023
b26bb16
Update tests for new addMembers return
Powersource May 5, 2023
c702a05
Add test for adding people to forked and old epochs
Powersource May 5, 2023
962bc09
Send old secrets when adding a new member
Powersource May 5, 2023
65456c9
Consume old secrets we're given
Powersource May 8, 2023
b120eba
Check that sent oldSecrets is correct
Powersource May 8, 2023
61ac83b
Update addMembers signature in readme
Powersource May 8, 2023
6bfd12b
Fix create test
Powersource May 8, 2023
72bed4b
Remove log
Powersource May 8, 2023
05fc30f
Update spec with oldSecrets
Powersource May 9, 2023
d295dd7
Merge branch 'master' of github.com:ssbc/ssb-tribes2 into added-late
Powersource May 9, 2023
f2e36e6
Use normal db2 config for many-publish test
Powersource May 9, 2023
14d46c8
Don't send old secrets on readditions
Powersource May 10, 2023
dab6bf4
Make simpler version of getPredecessorEpochs
Powersource May 11, 2023
5b3ca84
Merge branch 'master' of github.com:ssbc/ssb-tribes2 into added-late
Powersource May 11, 2023
ebe3d7f
Timeout publish many test
Powersource May 16, 2023
2b21cb7
Add a lot of assertions to new test
Powersource May 16, 2023
23ab8c1
Add timeouts
Powersource May 16, 2023
2b6871e
Remove comments
Powersource May 16, 2023
23c8708
More thorough replication in test
Powersource May 16, 2023
ef57736
More replication and timeouts
Powersource May 16, 2023
37f4523
Use normal db2 config
Powersource May 16, 2023
43f6eed
Unparallelize replication
Powersource May 16, 2023
013b9f4
Careful replication
Powersource May 16, 2023
a280bad
Log more on replication
Powersource May 17, 2023
6f1b144
Replicate extra much
Powersource May 17, 2023
d457046
Replicate in the correct order
Powersource May 17, 2023
0daab96
Tweaks for CI
mixmix May 18, 2023
c76ead4
replace replicate-all
mixmix May 18, 2023
4c42d82
change unspecified names
mixmix May 18, 2023
5c33a1f
Merge pull request #117 from ssbc/added-late_CI_FIXES
Powersource May 18, 2023
cadcbe7
Add warning comment to replicate
Powersource May 18, 2023
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
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>)`
mixmix marked this conversation as resolved.
Show resolved Hide resolved

### `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,
mixmix marked this conversation as resolved.
Show resolved Hide resolved
root,
creator: rootAuthorId,
text: opts?.text,
recps: [groupId, ...feedIds],
}
mixmix marked this conversation as resolved.
Show resolved Hide resolved
return cb(null, content)
}
)
}),
pull.asyncMap((content, cb) => publish(content, options, cb)),
mixmix marked this conversation as resolved.
Show resolved Hide resolved
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
53 changes: 52 additions & 1 deletion 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']) }
mixmix marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -384,6 +433,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
2 changes: 1 addition & 1 deletion listeners.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ module.exports = function startListeners(ssb, getPreferredEpoch, onError) {
(msg) => {
const groupId = msg.value.content.recps[0]

const secret = Buffer.from(msg.value.content.groupKey, 'base64')
const secret = Buffer.from(msg.value.content.secret, 'base64')
ssb.box2.addGroupInfo(groupId, { key: secret }, (err) => {
// prettier-ignore
if (err && !isClosed) return onError(clarify(err, 'Cannot add new epoch key that we found'))
Expand Down
Loading