-
-
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
[WIP] Signatures and encryption: Preliminary work #1257
Conversation
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.
Very nice work @corrideat, excellent progress. 👍
Preliminary review done! Please see new comments as well as old unaddressed ones.
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.
.
309ad07
to
c6802d4
Compare
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.
Prelim review continued!
contractName: string; | ||
contractID: string; | ||
data: GIOpKeyRequestResponse; | ||
signingKeyId: 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.
With so many types, maybe it makes sense to move them into their own individual types.js
file?
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.
Yes, that sounds reasonable.
- Volatile state (for keys) in simulated state - Remove state when logging out
/* passPayload = createInvite({ | ||
invitee: this.proposal.data.proposalData.member, | ||
creator: this.proposal.meta.username, | ||
expires: this.currentGroupState.settings.inviteExpiryProposal | ||
}) | ||
}) */ |
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.
Whenever you're ready to clean up code, if you could address this please (just a reminder to not forget it)
manifest, | ||
// the nonce makes it difficult to predict message contents | ||
// and makes it easier to prevent conflicts during development | ||
nonce: Math.random() |
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 did you get rid of the nonce
?
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.
Because: (1) all signed messages (which should be all messages) should already be unique due to the randomness in the signature, (2) the nonce doesn't really make it difficult to predict message contents, (2 bis) even if it did, Math.random()
isn't really unpredictable, (3) predicting message contents isn't bad in itself as the DB should be assumed to be public, (4) conflicts due to missing a nonce most certainly mean there's a bug.
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, I think I'm going to add it back in because of #1503 and it sounds like there's nothing wrong with it.
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.
Well, Math.random()
was probably something that was wrong with it (if it needs to be cryptographically secure at least). I think that #1503 / #1486 probably point out to other issues.
Messages can be re-created by 'chelonia/private/out/publishEvent', causing the hash of the successful message to be different, resulting in a hanging promise.
I'd say that the conditions that lead to a hanging promise seem to be the issue here, as not having a nonce isn't the only reason why this might happen. In other words, hanging promises should be handled for what they are, but that's a separate issue.
Other than that, I don't think a nonce is actively bad here, just unnecessary. The danger is relying on it being unique, which can't be guaranteed (realistically).
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.
Just an FYI this nonce
was recently added back in a recent PR. In the future, if we want to get rid of it, it will require changing how chat pending messages work by setting the id
in the data when creating a chat message. Each chat message creation would need to generate its own unique ID as part of the data of the message, and then it wouldn't need to rely on the GIMessage .id()
API
const recordPepper = 'pepper' | ||
const recordMasterKey = 'masterKey' | ||
const challengeSecret = 'secret' | ||
const registrationSecret = 'secret' |
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.
We don't want to have server admins dealing with anything related to cryptography unless for some reason they must and there's literally no way around it.
So what are these values and how can we avoid making the admin set them via configs?
It's unclear to me if these values must be different from server to server or whether they don't need to be. The consequences / explanations aren't explained anywhere.
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.
So what are these values and how can we avoid making the admin set them via configs?
These are secrets the server uses internally for deriving certain values that must be opaque to the client. As these values need to be persistent, I can't come up with a way of avoiding them entirely (although a solution could be for them to be set to some default value).9
const recordPepper = 'pepper'
Perhaps the least security-critical value. Used to derive a record ID which clients / 3rd parties cannot guess for storing the ZKPP
const recordMasterKey = 'masterKey'
Used to encrypt ZKPP records. If this value is leaked, 3rd parties with access to the database could defeat the protocol.
const challengeSecret = 'secret'
If this value is known, the protocol can be defeated by providing valid answers to challenges.
const registrationSecret = 'secret'
Similarly, but for registration.
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.
So what about making them random and storing them locally in files? Would that work?
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.
Sure, it could be a part of the installation process, and could be automated.
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.
I'm going to use this convention to mark threads as needing their commentary transferred (as comments) to the code.
➡️ code comments
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.
Note from call: we can get rid of recordPepper
, just use private/rid/${contractID}
challengeSecret
is the key to the keyed hash function in step 3 of the protocol.
@@ -190,7 +190,7 @@ export default ({ | |||
}) | |||
if (this.isDirectMessage(chatRoomId)) { | |||
this.updateCurrentChatRoomID(chatRoomId) | |||
} else if (chatRoomId && chatRoomId !== this.currentChatRoomId) { | |||
} else if (chatRoomId) { |
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.
I'm currently working on fixing a merge conflict with this.
What was the reason this change was made?
I'm leaning toward simply accepting the changes from Alex's DMs PR over this, but if there was a good reason for this would like to know.
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.
@Silver-IT maybe you can comment here as to the purpose of this && chatRoomId !== this.currentChatRoomId
?
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's nice catch, actually. As I added debounce to refreshContent
function in ChatMain.vue
, this is not needed.
I have found more unnecessary codes in this watch
, so I am going to update this in the next PR which I am currently working on. It's for fixing Issue #1493.
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.
I want you to just follow the current master branch so that I can optimize it later.
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.
async sideEffect ({ data, meta, contractID }, { state: Rstate }) { | ||
const rootState = sbp('state/vuex/state') | ||
const contracts = rootState.contracts || {} | ||
const { identityContractID } = rootState.loggedIn | ||
const userState = rootState[identityContractID] | ||
const state = rootState[contractID] | ||
|
||
if (rootState[data.chatRoomID]?._volatile) { | ||
return | ||
} | ||
|
||
await sbp('chelonia/out/keyRequest', { | ||
originatingContractID: identityContractID, | ||
originatingContractName: contracts[identityContractID].type, | ||
contractID: data.chatRoomID, | ||
contractName: 'gi.contracts/chatroom', | ||
signingKey: state._volatile?.keys?.[(((Object.values(Object(state._vm?.authorizedKeys)): any): GIKey[]).find((k) => k?.meta?.type === 'csk')?.id: ?string)], | ||
innerSigningKeyId: ((Object.values(userState._vm.authorizedKeys): any): GIKey[]).find((k) => k.meta?.type === 'csk')?.id, | ||
encryptionKeyId: ((Object.values(userState._vm.authorizedKeys): any): GIKey[]).find((k) => k.meta?.type === 'cek')?.id, | ||
hooks: { | ||
prepublish: null, | ||
postpublish: 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.
Could you add a comment explaining the specific scenario that's being handled here?
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.
I'll do; the specific scenario here is joining chatrooms (should be the general channel in this case) . This is required because joining a chatroom is a two-step process (i.e., first a group is joined, we get the list of chatrooms and then request to join) due to them being encrypted. In the future, this could be optimised so that multiple contracts can be 'joined' (for want of a better word) in a single operation
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.
Update: we agreed that this code should be removed so that a different method of obtaining the chatroom key can be implemented instead (by pre-sharing the key in the group contract using OP_KEYSHARE)
originatingContractName: contracts[identityContractID].type, | ||
contractID: data.chatRoomID, | ||
contractName: 'gi.contracts/chatroom', | ||
signingKey: state._volatile?.keys?.[(((Object.values(Object(state._vm?.authorizedKeys)): any): GIKey[]).find((k) => k?.meta?.type === 'csk')?.id: ?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.
Which key is being used here? It seems to say "pick the first CSK of the authorizedKeys"? Is that right? If so, why would that be correct? If not, what is going on?
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's correct: it does "pick the first CSK of the authorizedKeys". Not sure what you mean by 'why would that be correct?' though. What scenarios do you have in mind that would not make this correct? If it's the "pick the first CSK" part that's confusing, you can read it as "pick the first CSK", as there should be only one CSK at any given time (perhaps need to check the key management code to ensure this holds, but it should hold).
contractID: data.chatRoomID, | ||
contractName: 'gi.contracts/chatroom', | ||
signingKey: state._volatile?.keys?.[(((Object.values(Object(state._vm?.authorizedKeys)): any): GIKey[]).find((k) => k?.meta?.type === 'csk')?.id: ?string)], | ||
innerSigningKeyId: ((Object.values(userState._vm.authorizedKeys): any): GIKey[]).find((k) => k.meta?.type === 'csk')?.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.
Similar question here, a comment would be helpful here explaining which key is being picked and why.
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 finding the CSK by name under the assumption that there is only one CSK (which holds for group contracts)
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.
Seems like something that we should have a cleaner way of doing?
signatureFn?: Function = defaultSignatureFn | ||
{ | ||
contractID, | ||
originatingContractID, |
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.
What is this originatingContractID
, and which Wiki document discusses it?
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.
contractID
is the contract being modified. originatingContractID
is the contract initiating the interaction (used for cross-contract communication). In other words, contractID
is self-referential in a given contract and originatingContractID
refers to the contract that wrote to it. This is documented in the Key sharing document.
message | ||
head, | ||
message, | ||
decryptedValue: op.length === 3 ? op[2] : op[1], |
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 decryptedValue
always being set?
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 relevant commit: 2f70afa
In particular, see the change made to frontend/views/containers/chatroom/ChatMain.vue
Decrypted value is required to introspect messages. For compatibility purposes with the previous API, that didn't use decryptedValue API as an argument, decryptedValue is set automatically. This was for being cautious and is likely the argument can be assumed to be present.
Closing in favor of #1521 |
No description provided.