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

[WIP] Signatures and encryption: Preliminary work #1257

Closed
wants to merge 27 commits into from

Conversation

corrideat
Copy link
Member

No description provided.

frontend/utils/crypto.js Outdated Show resolved Hide resolved
frontend/utils/crypto.js Outdated Show resolved Hide resolved
frontend/utils/crypto.js Outdated Show resolved Hide resolved
frontend/utils/crypto.js Outdated Show resolved Hide resolved
frontend/utils/crypto.js Outdated Show resolved Hide resolved
frontend/utils/crypto.test.js Outdated Show resolved Hide resolved
shared/domains/chelonia/GIMessage.js Outdated Show resolved Hide resolved
shared/domains/chelonia/chelonia.js Outdated Show resolved Hide resolved
shared/domains/chelonia/chelonia.js Outdated Show resolved Hide resolved
.flowconfig Outdated Show resolved Hide resolved
Copy link
Member

@taoeffect taoeffect left a 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.

Copy link
Member Author

@corrideat corrideat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

@taoeffect taoeffect marked this pull request as draft August 15, 2022 10:55
@corrideat corrideat force-pushed the feature/password-state-new branch from 309ad07 to c6802d4 Compare August 15, 2022 12:11
Copy link
Member

@taoeffect taoeffect left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prelim review continued!

backend/routes.js Outdated Show resolved Hide resolved
backend/routes.js Outdated Show resolved Hide resolved
backend/routes.js Outdated Show resolved Hide resolved
backend/routes.js Outdated Show resolved Hide resolved
backend/zkppSalt.js Show resolved Hide resolved
shared/domains/chelonia/internals.js Show resolved Hide resolved
shared/domains/chelonia/internals.js Outdated Show resolved Hide resolved
shared/domains/chelonia/internals.js Outdated Show resolved Hide resolved
shared/domains/chelonia/internals.js Show resolved Hide resolved
shared/domains/chelonia/internals.js Outdated Show resolved Hide resolved
contractName: string;
contractID: string;
data: GIOpKeyRequestResponse;
signingKeyId: string;
Copy link
Member

@taoeffect taoeffect Nov 4, 2022

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?

Copy link
Member Author

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
Comment on lines +109 to +113
/* passPayload = createInvite({
invitee: this.proposal.data.proposalData.member,
creator: this.proposal.meta.username,
expires: this.currentGroupState.settings.inviteExpiryProposal
})
}) */
Copy link
Member

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()
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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).

Copy link
Member

@taoeffect taoeffect Mar 30, 2023

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

Comment on lines +9 to +12
const recordPepper = 'pepper'
const recordMasterKey = 'masterKey'
const challengeSecret = 'secret'
const registrationSecret = 'secret'
Copy link
Member

@taoeffect taoeffect Feb 16, 2023

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.

Copy link
Member Author

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.

Copy link
Member

@taoeffect taoeffect Mar 3, 2023

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?

Copy link
Member Author

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.

Copy link
Member

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

Copy link
Member

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) {
Copy link
Member

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.

Copy link
Member

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 ?

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Silver-IT 👍

Comment on lines +1151 to +1174
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
}
})
Copy link
Member

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?

Copy link
Member Author

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

Copy link
Member

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)],
Copy link
Member

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?

Copy link
Member Author

@corrideat corrideat Apr 1, 2023

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,
Copy link
Member

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.

Copy link
Member Author

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)

Copy link
Member

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,
Copy link
Member

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?

Copy link
Member Author

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],
Copy link
Member

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?

Copy link
Member Author

@corrideat corrideat Apr 1, 2023

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.

@taoeffect
Copy link
Member

Closing in favor of #1521

@taoeffect taoeffect closed this May 5, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants