-
-
Notifications
You must be signed in to change notification settings - Fork 44
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
PEK rotation improvements. #2584
base: master
Are you sure you want to change the base?
PEK rotation improvements. #2584
Conversation
Closes #2570.
backend/push.js
Outdated
@@ -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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be console.debug or something higher like log or info?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe. I thought debug
is appropriate since it's by definition a user-defined endpoint. Nothing stops a user from reporting an endpoint like https://www.bankofamerica.com/
, and since it's a third-party endpoint, we also can't control the SLA.
group-income
|
Project |
group-income
|
Branch Review |
2570-failure-to-show-username-for-contract-upon-join
|
Run status |
|
Run duration | 12m 13s |
Commit |
|
Committer | Ricardo Iván Vieitez Parra |
View all properties for this run ↗︎ |
Test results | |
---|---|
|
0
|
|
0
|
|
10
|
|
0
|
|
115
|
View all changes introduced in this branch ↗︎ |
@@ -947,6 +947,8 @@ export default ({ | |||
} | |||
sbp('gi.actions/identity/kv/setChatRoomReadUntil', { | |||
contractID: chatRoomID, messageHash, createdHeight | |||
}).catch(e => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bugfix
@@ -537,6 +538,20 @@ export default (sbp('sbp/selectors/register', { | |||
const keyId = findSuitableSecretKeyId(contractIDOrState, permissions, purposes, ringLevel, allowedActions) | |||
return keyId | |||
}, | |||
'chelonia/contract/setPendingKeyRevocation': function (contractID: string, names: string[]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Improvement as per contract TODOs.
@@ -1693,9 +1708,17 @@ async function outEncryptedOrUnencryptedAction ( | |||
if (opType === GIMessage.OP_ACTION_ENCRYPTED && !params.encryptionKeyId) { | |||
throw new Error('OP_ACTION_ENCRYPTED requires an encryption key ID be given') | |||
} | |||
if (params.encryptionKey) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This new parameter is needed to designate a raw key to use for encryption in OP_ATOMIC
. The current implementation of GIMessage
/ encryptedData
/ signedData
/ chelonia
isn't advanced enough to automatically switch to the new key on outgoing messages. The workaround is designating the key as a raw key.
@@ -108,7 +108,7 @@ const decryptData = function (height: number, data: any, additionalKeys: Object, | |||
const key = additionalKeys[eKeyId] | |||
|
|||
if (!key) { | |||
throw new ChelErrorDecryptionKeyNotFound(`Key ${eKeyId} not found`) | |||
throw new ChelErrorDecryptionKeyNotFound(`Key ${eKeyId} not found`, { cause: eKeyId }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding the cause
here helps identify the key that failed.
test/cypress/support/commands.js
Outdated
@@ -113,7 +113,7 @@ Cypress.Commands.add('getByDT', (element, otherSelector = '') => { | |||
cySbpCheckCommand('giNoPendingGroupKeyShares', (sbp) => { | |||
const state = sbp('state/vuex/state') | |||
const pending = Object.keys(state.contracts) | |||
.filter(contractID => state[contractID]._vm.type === 'gi.contracts/group') | |||
.filter(contractID => state[contractID]._vm?.type === 'gi.contracts/group') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For some reason, sometimes _vm
was undefined in the tests, causing this to fail.
shared/domains/chelonia/internals.js
Outdated
@@ -2091,6 +2116,15 @@ const handleEvent = { | |||
} | |||
this.config.reactiveSet(state.contracts[contractID], 'HEAD', hash) | |||
this.config.reactiveSet(state.contracts[contractID], 'height', height) | |||
if (missingDecryptionKeyId) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This adds a log of missed decryption keys, which can be used to more accurately determine when a contract needs to be re-synced.
shared/domains/chelonia/internals.js
Outdated
@@ -2046,17 +2066,22 @@ const handleEvent = { | |||
return acc | |||
} | |||
|
|||
const actionsOpV = ((msg: any): GIOpAtomic).reduce(reducer, []) | |||
const actionsOpV = (Number.isFinite(errorIndex) | |||
// $FlowFixMe[incompatible-call] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The side-effects sometimes get called if there was an error (i.e., ChelErrorWarning
has this behaviour). This only executes side-effects up to the point where process succeeded in OP_ATOMIC
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, so based on the call here's how we've agreed conceptually this code should work:
There are two types of "errors" that we need to consider:
- "Ignoring" errors
- "Failure" errors
Example: OP_KEY_ADD
- IGNORING: an error is thrown because we wanted to add a key but the key we wanted to add is already there. This is not a hard error, it's an ignoring error. We don't care that the operation failed in this case because the intent was accomplished.
- FAILURE: an error is thrown because we wanted to add a key that doesn't exist.
Example: OP_ACTION_ENCRYPTED
- IGNORING: An error is thrown because we don't have the key to decrypt the action. We ignore it.
- FAILURE: An error is thrown by the process function during processing.
Handling these in OP_ATOMIC
- ALL errors of class "IGNORING" should be ignored. They should not impact our ability to process the rest of the operations in the
OP_ATOMIC
. No matter how many of these are thrown, it doesn't affect the rest of the operations. - ANY error of class "FAILURE" will call the rest of the operations to fail and the state to be reverted to prior to the
OP_ATOMIC
. No side-effects should be run. Because an intention failed.
Feel free to copy this 👆 as a comment in the code.
shared/domains/chelonia/internals.js
Outdated
} | ||
await opFns[u[0]](u[1]) | ||
} catch (e) { | ||
e[errorIndexSymbol] = i |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Point where the error occurred.
return | ||
} | ||
const attributes = state[contractID].attributes | ||
const attributes = state[contractID]?.attributes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This fixes the ProfileCard not showing if the username isn't known.
@@ -49,6 +49,16 @@ console.info('CONTRACTS_VERSION:', process.env.CONTRACTS_VERSION) | |||
console.info('LIGHTWEIGHT_CLIENT:', process.env.LIGHTWEIGHT_CLIENT) | |||
console.info('NODE_ENV:', process.env.NODE_ENV) | |||
|
|||
if (process.env.CI) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add logging information on CI to get a better stack trace and identify missing error handling.
frontend/controller/actions/index.js
Outdated
@@ -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] ?? []), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Function updated to support arbitrary operations. This also makes this function generic and no longer GI-specific.
return [ | ||
undefined, // Nothing before OP_KEY_UPDATE | ||
[ | ||
// Re-encrypt attributes with the new PEK |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is 'the fix' for this issue, i.e., re-encrypting attributes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Preliminary review based on call ready!
shared/domains/chelonia/internals.js
Outdated
@@ -2046,17 +2066,22 @@ const handleEvent = { | |||
return acc | |||
} | |||
|
|||
const actionsOpV = ((msg: any): GIOpAtomic).reduce(reducer, []) | |||
const actionsOpV = (Number.isFinite(errorIndex) | |||
// $FlowFixMe[incompatible-call] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, so based on the call here's how we've agreed conceptually this code should work:
There are two types of "errors" that we need to consider:
- "Ignoring" errors
- "Failure" errors
Example: OP_KEY_ADD
- IGNORING: an error is thrown because we wanted to add a key but the key we wanted to add is already there. This is not a hard error, it's an ignoring error. We don't care that the operation failed in this case because the intent was accomplished.
- FAILURE: an error is thrown because we wanted to add a key that doesn't exist.
Example: OP_ACTION_ENCRYPTED
- IGNORING: An error is thrown because we don't have the key to decrypt the action. We ignore it.
- FAILURE: An error is thrown by the process function during processing.
Handling these in OP_ATOMIC
- ALL errors of class "IGNORING" should be ignored. They should not impact our ability to process the rest of the operations in the
OP_ATOMIC
. No matter how many of these are thrown, it doesn't affect the rest of the operations. - ANY error of class "FAILURE" will call the rest of the operations to fail and the state to be reverted to prior to the
OP_ATOMIC
. No side-effects should be run. Because an intention failed.
Feel free to copy this 👆 as a comment in the code.
if (!state.chatRooms) { | ||
// When creating a DM, we may not have the `.chatRooms` property | ||
state.chatRooms = Object.create(null) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would we not have the .chatRooms
property? It's created by the constructor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That requires the constructor being called. It won't be if the decryption key isn't available (for example, because it's been rotated).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add a comment here explaining this? I think most people reading this code will have no idea about this.
} | ||
await opFns[u[0]](u[1]) | ||
} catch (e) { | ||
if (e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is there an if (e)
thing here...? In what situation can this not be defined?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
throw undefined
, throw null
, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't it still make sense then to do something here, like log a console.error
in an else { .. }
branch?
@@ -801,7 +845,9 @@ export default (sbp('sbp/selectors/register', { | |||
key: deserializeKey(decrypted), | |||
transient | |||
}])) | |||
if ( | |||
if (missingDecryptionKeyIds?.includes(key.id)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a comment here explaining what this is doing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My review is ready!
Claude's review of internals.js
After reviewing the diff and the full file, here are a few potential issues and observations:
- In the new code for handling errors in
OP_ATOMIC
, there's a potential issue with the missingDecryptionKeyIds handling. The code checks ifmissingDecryptionKeyIds
is a Set, but later in the file, it's treated as an array. This inconsistency could lead to runtime errors.
// In OP_ATOMIC:
const missingDecryptionKeyIds = missingDecryptionKeyIdsMap.get(message)
if (missingDecryptionKeyIds) {
missingDecryptionKeyIds.add(e.cause)
} else {
missingDecryptionKeyIdsMap.set(message, new Set([e.cause]))
}
// Later in applyProcessResult:
if (Array.isArray(missingDecryptionKeyIdsForMessage)) { // This expects an array, not a Set
// ...
}
- The error handling in
OP_ATOMIC
might have an issue with the nested conditions:
if (e) {
if (e.name === 'ChelErrorDecryptionKeyNotFound') {
// ...
} else {
// ...
}
// ...
if (e.name === 'ChelErrorWarning') continue
}
throw e
The throw e
is outside the first if (e)
block, which means it will always throw the error even if e
is falsy. It should probably be inside the first if (e)
block.
-
There's a potential memory leak with the WeakMap usage. While WeakMaps are good for garbage collection, the code should ensure that all message objects are properly cleaned up when they're no longer needed. The code does include cleanup in the
finally
block, but it might be worth adding additional cleanup in error cases. -
In the new code that handles missing decryption key IDs, there's a duplicate height update:
if (Array.isArray(missingDecryptionKeyIdsForMessage)) {
// ...
missingDecryptionKeyIdsForMessage.forEach(keyId => {
if (missingDecryptionKeyIds.includes(keyId)) return
missingDecryptionKeyIds.push(keyId)
})
this.config.reactiveSet(state.contracts[contractID], 'height', height) // This is redundant
}
This height update is redundant because it's already being set earlier in the function:
this.config.reactiveSet(state.contracts[contractID], 'height', height)
These issues should be addressed to improve the robustness of the code.
Gemini 2.0 Flash Thinking's Review of internals.js
// TODO: Sometimes it seems to get stuck here when bypassing the UI | ||
bypassUI: false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't seem like this fixed the failing build, so can this be returned to true
?
- Prior commit failure: https://cloud.cypress.io/projects/q6whky/runs/3885/test-results?actions=%5B%5D&browsers=%5B%5D&groups=%5B%5D&isFlaky=%5B%5D&modificationDateRange=%7B%22startDate%22%3A%221970-01-01%22%2C%22endDate%22%3A%222038-01-19%22%7D&orderBy=EXECUTION_ORDER&oses=%5B%5D&specs=%5B%5D&statuses=%5B%7B%22value%22%3A%22FAILED%22%2C%22label%22%3A%22FAILED%22%7D%5D&testingTypesEnum=%5B%5D
- This commit: https://cloud.cypress.io/projects/q6whky/runs/3886/test-results?actions=%5B%5D&browsers=%5B%5D&groups=%5B%5D&isFlaky=%5B%5D&modificationDateRange=%7B%22startDate%22%3A%221970-01-01%22%2C%22endDate%22%3A%222038-01-19%22%7D&orderBy=EXECUTION_ORDER&oses=%5B%5D&specs=%5B%5D&statuses=%5B%7B%22value%22%3A%22FAILED%22%2C%22label%22%3A%22FAILED%22%7D%5D&testingTypesEnum=%5B%5D
it('user3 can see the username for userBot', () => { | ||
cy.giLogin(`user3-${userId}`, { bypassUI: true }) | ||
assertMembersCount(3) | ||
|
||
cy.getByDT('groupMembers').contains('userbot-') | ||
cy.window().its('sbp').then(sbp => { | ||
const rootState = sbp('state/vuex/state') | ||
const userbotId = rootState.namespaceLookups[`userbot-${userId}`] | ||
const PEKs = Object.values(rootState[userbotId]._vm.authorizedKeys).filter(key => key.name === 'pek') | ||
if (PEKs.length < 2) throw new RangeError('Expected the PEK to have been rotated') | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice tests 👍
Closes #2570.
Testing instructions:
alice
,bob
,carol
)alice
creates a groupG1
bob
creates a groupG2
carol
joinsG1
carol
leavesG1
. There are no warning or errors for/rotateKeys
. Thepek
forcarol
(in_vm.authorizedKeys
) has been rotated.carol
joinsG2
bob
observes that the username forcarol
appears inG2
.