Skip to content

Commit

Permalink
PEK rotation improvements.
Browse files Browse the repository at this point in the history
Closes #2570.
  • Loading branch information
corrideat committed Feb 1, 2025
1 parent 3a128a4 commit e08995f
Show file tree
Hide file tree
Showing 13 changed files with 147 additions and 96 deletions.
1 change: 1 addition & 0 deletions backend/push.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ export const postEvent = async (subscription: Object, event: ?string): Promise<v
})

if (!req.ok) {
console.debug('Error sending push notification', subscription.id, req.status)
// If the response was 401 (Unauthorized), 404 (Not found) or 410 (Gone),
// it likely means that the subscription no longer exists.
if ([401, 404, 410].includes(req.status)) {
Expand Down
38 changes: 22 additions & 16 deletions frontend/controller/actions/chatroom.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,32 +215,38 @@ export default (sbp('sbp/selectors/register', {
await sbp('chelonia/contract/release', userID, { ephemeral: true })
}
},
'gi.actions/chatroom/shareNewKeys': (contractID: string, newKeys) => {
'gi.actions/chatroom/shareNewKeys': async (contractID: string, newKeys) => {
const state = sbp('chelonia/contract/state', contractID)
const mainCEKid = await sbp('chelonia/contract/currentKeyIdByName', state, 'cek')

const originatingContractID = state.attributes.groupContractID ? state.attributes.groupContractID : contractID

// $FlowFixMe
return Promise.all(Object.keys(state.members).map(async (pContractID) => {
return [Promise.all(Object.keys(state.members).map(async (pContractID) => {
const CEKid = await sbp('chelonia/contract/currentKeyIdByName', pContractID, 'cek')
if (!CEKid) {
console.warn(`Unable to share rotated keys for ${originatingContractID} with ${pContractID}: Missing CEK`)
return
}
return {
contractID,
foreignContractID: pContractID,
// $FlowFixMe
keys: Object.values(newKeys).map(([, newKey, newId]: [any, Key, string]) => ({
id: newId,
meta: {
private: {
content: encryptedOutgoingData(pContractID, CEKid, serializeKey(newKey, true))
}
}
}))
}
}))
return [
'chelonia/out/keyShare',
{
data: encryptedOutgoingData(contractID, mainCEKid, {
contractID,
foreignContractID: pContractID,
// $FlowFixMe
keys: Object.values(newKeys).map(([, newKey, newId]: [any, Key, string]) => ({
id: newId,
meta: {
private: {
content: encryptedOutgoingData(pContractID, CEKid, serializeKey(newKey, true))
}
}
}))
})
}
]
}))]
},
...encryptedNotification('gi.actions/chatroom/user-typing-event', L('Failed to send typing notification')),
...encryptedNotification('gi.actions/chatroom/user-stop-typing-event', L('Failed to send stopped typing notification')),
Expand Down
37 changes: 21 additions & 16 deletions frontend/controller/actions/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -626,12 +626,13 @@ export default (sbp('sbp/selectors/register', {
}
})
},
'gi.actions/group/shareNewKeys': (contractID: string, newKeys) => {
'gi.actions/group/shareNewKeys': async (contractID: string, newKeys) => {
const rootState = sbp('chelonia/rootState')
const state = rootState[contractID]
const mainCEKid = await sbp('chelonia/contract/currentKeyIdByName', state, 'cek')

// $FlowFixMe
return Promise.all(
return [Promise.all(
Object.entries(state.profiles)
.filter(([_, p]) => (p: any).status === PROFILE_STATUS.ACTIVE)
.map(async ([pContractID]) => {
Expand All @@ -640,20 +641,24 @@ export default (sbp('sbp/selectors/register', {
console.warn(`Unable to share rotated keys for ${contractID} with ${pContractID}: Missing CEK`)
return Promise.resolve()
}
return {
contractID,
foreignContractID: pContractID,
// $FlowFixMe
keys: Object.values(newKeys).map(([, newKey, newId]: [any, Key, string]) => ({
id: newId,
meta: {
private: {
content: encryptedOutgoingData(pContractID, CEKid, serializeKey(newKey, true))
}
}
}))
}
}))
return [
'chelonia/out/keyShare',
{
data: encryptedOutgoingData(contractID, mainCEKid, {
contractID,
foreignContractID: pContractID,
// $FlowFixMe
keys: Object.values(newKeys).map(([, newKey, newId]: [any, Key, string]) => ({
id: newId,
meta: {
private: {
content: encryptedOutgoingData(pContractID, CEKid, serializeKey(newKey, true))
}
}
}))
})
}]
}))]
},
...encryptedAction('gi.actions/group/addChatRoom', L('Failed to add chat channel'), async function (sendMessage, params) {
const rootState = sbp('chelonia/rootState')
Expand Down
15 changes: 14 additions & 1 deletion frontend/controller/actions/identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,20 @@ export default (sbp('sbp/selectors/register', {
// share along with OP_KEY_UPDATE. In this case, we're sharing all keys
// to their respective contracts and there are no keys to include in
// the same event as OP_KEY_UPDATE. Therefore, we return undefined
return undefined
if (!newKeys.pek) return undefined
return [
undefined, // Nothing before OP_KEY_UPDATE
[
// Re-encrypt attributes with the new PEK
await sbp('gi.actions/identity/setAttributes', {
contractID,
data: state.attributes,
encryptionKey: newKeys.pek[1],
encryptionKeyId: newKeys.pek[2],
returnInvocation: true
})
]
]
},
...encryptedAction('gi.actions/identity/setAttributes', L('Failed to set profile attributes.'), undefined, 'pek'),
...encryptedAction('gi.actions/identity/updateSettings', L('Failed to update profile settings.')),
Expand Down
9 changes: 3 additions & 6 deletions frontend/controller/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,6 @@ sbp('sbp/selectors/register', {
throw new Error('No suitable signing key found')
}

// TODO: GI-specific
const CEKid = findKeyIdByName(state, 'cek')
if (!CEKid) return

// Share new keys with other contracts
const keyShares = shareNewKeysSelector ? await sbp(shareNewKeysSelector, contractID, newKeys) : undefined

Expand All @@ -191,8 +187,9 @@ sbp('sbp/selectors/register', {
contractID,
contractName,
data: [
...keyShares.map((data) => ['chelonia/out/keyShare', { data: encryptedOutgoingData(contractID, CEKid, data) }]),
['chelonia/out/keyUpdate', { data: updatedKeys }]
...(keyShares[0] ?? []),
['chelonia/out/keyUpdate', { data: updatedKeys }],
...(keyShares[1] ?? [])
],
signingKeyId,
hooks: {
Expand Down
18 changes: 6 additions & 12 deletions frontend/model/contracts/chatroom.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ sbp('chelonia/defineContract', {
}

if (!itsMe && state.attributes.privacyLevel === CHATROOM_PRIVACY_LEVEL.PRIVATE) {
sbp('gi.contracts/chatroom/rotateKeys', contractID, state)
sbp('gi.contracts/chatroom/rotateKeys', contractID)
}

sbp('gi.contracts/chatroom/removeForeignKeys', contractID, memberID, state)
Expand Down Expand Up @@ -652,17 +652,11 @@ sbp('chelonia/defineContract', {
})
}
},
'gi.contracts/chatroom/rotateKeys': (contractID, state) => {
if (!state._volatile) state['_volatile'] = Object.create(null)
if (!state._volatile.pendingKeyRevocations) state._volatile['pendingKeyRevocations'] = Object.create(null)

const CSKid = findKeyIdByName(state, 'csk')
const CEKid = findKeyIdByName(state, 'cek')

state._volatile.pendingKeyRevocations[CSKid] = true
state._volatile.pendingKeyRevocations[CEKid] = true

sbp('gi.actions/out/rotateKeys', contractID, 'gi.contracts/chatroom', 'pending', 'gi.actions/chatroom/shareNewKeys').catch(e => {
'gi.contracts/chatroom/rotateKeys': (contractID) => {
sbp('chelonia/queueInvocation', contractID, async () => {
await sbp('chelonia/contract/setPendingKeyRevocation', contractID, ['cek', 'csk'])
await sbp('gi.actions/out/rotateKeys', contractID, 'gi.contracts/chatroom', 'pending', 'gi.actions/chatroom/shareNewKeys')
}).catch(e => {
console.warn(`rotateKeys: ${e.name} thrown during queueEvent to ${contractID}:`, e)
})
},
Expand Down
29 changes: 9 additions & 20 deletions frontend/model/contracts/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,17 +194,11 @@ function memberLeaves ({ memberID, dateLeft, heightLeft, ourselvesLeaving }, { c
// to be rotated. Later, this will be used by 'gi.contracts/group/rotateKeys'
// (to actually perform the rotation) and Chelonia (to unset the flag if
// they are rotated by somebody else)
// TODO: Improve this API. Developers should not modify state that is managed
// by Chelonia.
// Example: sbp('chelonia/contract/markKeyForRevocation', contractID, 'csk')
if (!state._volatile) state['_volatile'] = Object.create(null)
if (!state._volatile.pendingKeyRevocations) state._volatile['pendingKeyRevocations'] = Object.create(null)

const CSKid = findKeyIdByName(state, 'csk')
const CEKid = findKeyIdByName(state, 'cek')

state._volatile.pendingKeyRevocations[CSKid] = true
state._volatile.pendingKeyRevocations[CEKid] = true
sbp('chelonia/queueInvocation', contractID, () => {
return sbp('chelonia/contract/setPendingKeyRevocation', contractID, ['cek', 'csk'])
}).catch(e => {
console.warn('[memberLeaves] Error marking key for revocation', e)
})
}

function isActionNewerThanUserJoinedDate (height: number, userProfile: ?Object): boolean {
Expand Down Expand Up @@ -1765,16 +1759,11 @@ sbp('chelonia/defineContract', {
'gi.contracts/group/revokeGroupKeyAndRotateOurPEK': (groupContractID) => {
const rootState = sbp('state/vuex/state')
const { identityContractID } = rootState.loggedIn
const state = rootState[identityContractID]

if (!state._volatile) state['_volatile'] = Object.create(null)
if (!state._volatile.pendingKeyRevocations) state._volatile['pendingKeyRevocations'] = Object.create(null)

const PEKid = findKeyIdByName(state, 'pek')

state._volatile.pendingKeyRevocations[PEKid] = true

sbp('chelonia/queueInvocation', identityContractID, ['gi.actions/out/rotateKeys', identityContractID, 'gi.contracts/identity', 'pending', 'gi.actions/identity/shareNewPEK']).catch(e => {
sbp('chelonia/queueInvocation', identityContractID, async () => {
await sbp('chelonia/contract/setPendingKeyRevocation', identityContractID, ['pek'])
await sbp('gi.actions/out/rotateKeys', identityContractID, 'gi.contracts/identity', 'pending', 'gi.actions/identity/shareNewPEK')
}).catch(e => {
console.warn(`revokeGroupKeyAndRotateOurPEK: ${e.name} thrown during queueEvent to ${identityContractID}:`, e)
})
},
Expand Down
30 changes: 19 additions & 11 deletions frontend/model/contracts/identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ sbp('chelonia/defineContract', {
}
},
process ({ data }, { state }) {
if (!state.attributes) {
// If it's not our own identity contract, attributes may not be
// defined
state.attributes = Object.create(null)
}
for (const key in data) {
state.attributes[key] = data[key]
}
Expand All @@ -142,6 +147,11 @@ sbp('chelonia/defineContract', {
}
},
process ({ data }, { state }) {
if (!state.attributes) {
// If it's not our own identity contract, attributes may not be
// defined
return
}
for (const attribute of data) {
delete state.attributes[attribute]
}
Expand Down Expand Up @@ -180,6 +190,10 @@ sbp('chelonia/defineContract', {
process ({ data }, { state }) {
// NOTE: this method is always created by another
const { contractID } = data
if (!state.chatRooms) {
// When creating a DM, we may not have the `.chatRooms` property
state.chatRooms = Object.create(null)
}
if (state.chatRooms[contractID]) {
throw new TypeError(L('Already joined direct message.'))
}
Expand Down Expand Up @@ -388,14 +402,8 @@ sbp('chelonia/defineContract', {
},
methods: {
'gi.contracts/identity/revokeGroupKeyAndRotateOurPEK': (identityContractID, state, groupContractID) => {
if (!state._volatile) state['_volatile'] = Object.create(null)
if (!state._volatile.pendingKeyRevocations) state._volatile['pendingKeyRevocations'] = Object.create(null)

const CSKid = findKeyIdByName(state, 'csk')
const CEKid = findKeyIdByName(state, 'cek')
const PEKid = findKeyIdByName(state, 'pek')

state._volatile.pendingKeyRevocations[PEKid] = true

const groupCSKids = findForeignKeysByContractID(state, groupContractID)

Expand All @@ -415,11 +423,11 @@ sbp('chelonia/defineContract', {
})
}

sbp('chelonia/queueInvocation', identityContractID, ['chelonia/contract/disconnect', identityContractID, groupContractID]).catch(e => {
console.warn(`revokeGroupKeyAndRotateOurPEK: ${e.name} thrown during queueEvent to ${identityContractID}:`, e)
})

sbp('chelonia/queueInvocation', identityContractID, ['gi.actions/out/rotateKeys', identityContractID, 'gi.contracts/identity', 'pending', 'gi.actions/identity/shareNewPEK']).catch(e => {
sbp('chelonia/queueInvocation', identityContractID, async () => {
await sbp('chelonia/contract/setPendingKeyRevocation', identityContractID, ['pek'])
await sbp('gi.actions/out/rotateKeys', identityContractID, 'gi.contracts/identity', 'pending', 'gi.actions/identity/shareNewPEK')
await sbp('chelonia/contract/disconnect', identityContractID, groupContractID)
}).catch(e => {
console.warn(`revokeGroupKeyAndRotateOurPEK: ${e.name} thrown during queueEvent to ${identityContractID}:`, e)
})
},
Expand Down
16 changes: 8 additions & 8 deletions frontend/model/getters.js
Original file line number Diff line number Diff line change
Expand Up @@ -461,23 +461,23 @@ const getters: { [x: string]: (state: Object, getters: { [x: string]: any }) =>
Object.keys(state.contracts)
.filter(contractID => state.contracts[contractID].type === 'gi.contracts/identity')
.forEach(contractID => {
if (!state[contractID]) {
console.warn('[ourContactProfilesById] Missing state', contractID)
return
}
const attributes = state[contractID].attributes
const attributes = state[contractID]?.attributes
if (attributes) { // NOTE: this is for fixing the error while syncing the identity contracts
const username = checkedUsername(state, attributes.username, contractID)
profiles[contractID] = {
...attributes,
username,
contractID
}
} else {
profiles[contractID] = {
contractID
}
}
})
// For consistency, add users that were known in the past (since those
// contracts will be removed). This keeps mentions working in existing
// devices
// For consistency, add users that were known in the past (since those
// contracts will be removed). This keeps mentions working in existing
// devices
Object.keys(state.reverseNamespaceLookups).forEach((contractID) => {
if (profiles[contractID]) return
profiles[contractID] = {
Expand Down
1 change: 1 addition & 0 deletions frontend/setupChelonia.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ const setupChelonia = async (): Promise<*> => {
'state/vuex/state', 'state/vuex/settings', 'state/vuex/commit', 'state/vuex/getters',
'chelonia/rootState', 'chelonia/contract/state', 'chelonia/contract/sync', 'chelonia/contract/isSyncing', 'chelonia/contract/remove', 'chelonia/contract/retain', 'chelonia/contract/release', 'controller/router',
'chelonia/contract/suitableSigningKey', 'chelonia/contract/currentKeyIdByName',
'chelonia/contract/setPendingKeyRevocation',
'chelonia/storeSecretKeys', 'chelonia/crypto/keyId',
'chelonia/queueInvocation', 'chelonia/contract/wait',
'chelonia/contract/waitingForKeyShareTo',
Expand Down
Loading

0 comments on commit e08995f

Please sign in to comment.