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

Password changes #2446

Merged
merged 19 commits into from
Dec 18, 2024
Merged

Password changes #2446

merged 19 commits into from
Dec 18, 2024

Conversation

corrideat
Copy link
Member

@corrideat corrideat commented Dec 6, 2024

Closes #1923

@corrideat corrideat requested a review from taoeffect December 6, 2024 20:01
@corrideat corrideat self-assigned this Dec 6, 2024
Copy link

cypress bot commented Dec 6, 2024

group-income    Run #3597

Run Properties:  status check passed Passed #3597  •  git commit a335b1484b ℹ️: Merge e8f52016cca709df74fd77b186835100933171f6 into 6b1ad586cc42852aabfbc98ef52c...
Project group-income
Branch Review feature/password-changing
Run status status check passed Passed #3597
Run duration 11m 32s
Commit git commit a335b1484b ℹ️: Merge e8f52016cca709df74fd77b186835100933171f6 into 6b1ad586cc42852aabfbc98ef52c...
Committer Ricardo Iván Vieitez Parra
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 10
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 112
View all changes introduced in this branch ↗︎

@corrideat corrideat force-pushed the feature/password-changing branch from 7c5c849 to 6a5a0d5 Compare December 7, 2024 18:07
@corrideat corrideat force-pushed the feature/password-changing branch from b6358a9 to 140c826 Compare December 7, 2024 19:39
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.

Questions...

frontend/controller/actions/identity.js Outdated Show resolved Hide resolved
frontend/controller/actions/identity.js Outdated Show resolved Hide resolved
@corrideat corrideat force-pushed the feature/password-changing branch from bb86fa5 to e4d05fd Compare December 11, 2024 16:33
@corrideat corrideat force-pushed the feature/password-changing branch from e4d05fd to e88c818 Compare December 11, 2024 16:55
shared/zkpp.js Outdated Show resolved Hide resolved
shared/zkpp.js Outdated Show resolved Hide resolved
shared/zkpp.js 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.

Preliminary review ready!

Also, could you please add one test to signup-and-login.spec.js to test the changing of the password? I.e. it can do the following:

  1. After logging in, change the password.
  2. Log out
  3. Verify that logging back in with the old password doesn't work
  4. Verify that logging back in with the new password does work

@corrideat corrideat marked this pull request as ready for review December 14, 2024 15:29
@corrideat corrideat requested a review from taoeffect December 14, 2024 18:01
@corrideat
Copy link
Member Author

corrideat commented Dec 14, 2024

I believe this is ready to merge, for the most part. However, when you test it, you'll notice that the validation error messages seem to be default ones, like minLength. I can add the appropriate messages if you tell me what they should be.

@@ -116,7 +116,24 @@ route.POST('/event', {
}
}
}
const saltUpdateToken = request.headers['shelter-salt-update-token']
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 implements the atomic update using a token.

@@ -821,7 +838,7 @@ route.GET('/zkpp/{name}/contract_hash', {
return Boom.internal('internal error')
})

route.POST('/zkpp/updatePasswordHash/{name}', {
route.POST('/zkpp/{name}/updatePasswordHash', {
Copy link
Member Author

Choose a reason for hiding this comment

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

Renamed for consistency

@@ -60,10 +63,23 @@ export const initZkpp = async () => {
recordSecret = Buffer.from(hashStringArray('private/recordSecret', IKM)).toString('base64')
challengeSecret = Buffer.from(hashStringArray('private/challengeSecret', IKM)).toString('base64')
registrationSecret = Buffer.from(hashStringArray('private/registrationSecret', IKM)).toString('base64')
hashUpdateSecret = Buffer.from(hashStringArray('private/hashUpdateSecret', IKM)).toString('base64')
Copy link
Member Author

Choose a reason for hiding this comment

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

Used for encrypting the update token.

Copy link
Member

Choose a reason for hiding this comment

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

Are these strings like 'private/hashUpdateSecret' something that should be put in zkppConstants.js too?

Copy link
Member Author

Choose a reason for hiding this comment

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

Probably not, since they're only used here for initialisation purposes.

Copy link
Member Author

Choose a reason for hiding this comment

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

They also have no particular significance with regard to the protocol.

}

return (cid: ?string) => {
return setZkppSaltRecord(contract, hashedPassword, authSalt, contractSalt, cid)
Copy link
Member Author

Choose a reason for hiding this comment

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

Two-step process to allow validating the token first before making the change.

@@ -73,6 +73,7 @@
i18n.is-title-3(tag='h3' class='card-header') Delete account
p
i18n Deleting your account will erase all your data, and remove you from the groups you belong to.
| {{ ' ' }}
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 fixes a display issue (there was no space)

@@ -8,10 +8,20 @@ import { isRawSignedData, signedIncomingData } from './signedData.js'
const rootStateFn = () => sbp('chelonia/rootState')

export interface EncryptedData<T> {
// The ID of the encryption key used
Copy link
Member Author

Choose a reason for hiding this comment

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

Explanation you requested added here.


return true
return encryptContractSalt(c, JSON.stringify([oldContractSalt, token]))
Copy link
Member Author

Choose a reason for hiding this comment

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

Instead of returning true, we return the token for requesting an update. We also return the oldContractSalt because it's helpful for avoiding an extra request, as we need it to decrypt the old (current) IEK.

if (!Array.isArray(recordObj) || recordObj.length !== 3 || !recordObj.reduce((acc, cv) => acc && typeof cv === 'string', true)) {
if (
!Array.isArray(recordObj) ||
(recordObj.length !== 3 && recordObj.length !== 4) ||
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 because before the record was a triplet, and now it can be a triplet or a 4-tuple (with the 4th entry being a CID, if there's been a password update)

@@ -251,10 +273,10 @@ export const getContractSalt = async (contract: string, r: string, s: string, si
throw new Error('getContractSalt: Bad challenge')
}

return encryptContractSalt(c, contractSalt)
return encryptContractSalt(c, JSON.stringify([contractSalt, cid]))
Copy link
Member Author

Choose a reason for hiding this comment

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

We now return the contractSalt and the CID.

export default (sbp('sbp/selectors/register', {
'gi.actions/identity/create': async function ({
IPK,
IEK,
IPK: wIPK,
Copy link
Member Author

Choose a reason for hiding this comment

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

These changes aren't strictly related to this PR, rather something that slipped through the cracks in the past. This wraps secrets in Secret objects.

@@ -187,7 +187,41 @@ export default (sbp('sbp/selectors/register', {
'hc': Buffer.from(hc).toString('base64').replace(/\//g, '_').replace(/\+/g, '-').replace(/=*$/, '')
})).toString()}`).then(handleFetchResult('text'))

return decryptContractSalt(c, contractHash)
// [contractSalt, cid]
return JSON.parse(decryptContractSalt(c, contractHash))
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 because of the API change, now instead of just contractSalt, it could include a CID.

// TODO: Wrap IPK and IEK in "Secret"
IPK: serializeKey(IPK, true),
IEK: serializeKey(IEK, true),
IPK: new Secret(serializeKey(IPK, true)),
Copy link
Member Author

Choose a reason for hiding this comment

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

Implement the old TODO.


const [newContractSalt, oldContractSalt, updateToken] = await sbp('gi.app/identity/updateSaltRequest', username, wOldPassword, wNewPassword)

const oldIPK = await deriveKeyFromPassword(EDWARDS25519SHA512BATCH, oldPassword, oldContractSalt)
Copy link
Member Author

Choose a reason for hiding this comment

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

We do this here to avoid ever sending the password to the SW.

@@ -14,7 +14,6 @@ modal-template(class='is-centered is-left-aligned' back-on-mobile=true ref='moda
:value='form'
:$v='$v'
@enter='changePassword'
@input='(password) => { newPassword = password }'
Copy link
Member Author

Choose a reason for hiding this comment

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

These seemed to be wrong.

:disabled='$v.form.$invalid'
) Change password

template(slot='errors') {{ form.response }}
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 didn't work, and the slot doesn't exist. Replaced it with banner-scoped.

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.

Really impressive work @corrideat!

It seems to work well in my tests in almost every browser. However, there seems to be an issue with Safari:

Steps:

  1. Create account u1 in Firefox Dev Edition, with group
  2. Open Safari, attempt to sign in as u1

Results:

Screenshot 2024-12-15 at 5 02 03 PM

Server log shows:

[17:00:50.538] DEBUG (14007): 127.0.0.1: GET /assets/js/undefined/name/u1 --> 404
[17:01:15.697] DEBUG (14007): [pubsub] Pinging clients
[17:01:45.698] DEBUG (14007): [pubsub] Pinging clients
[17:02:15.510] DEBUG (14007): 127.0.0.1: GET /assets/js/undefined/name/u1 --> 404
[17:02:15.699] DEBUG (14007): [pubsub] Pinging clients

Comment on lines +130 to +132
})().finally(() => {
this.processing = false
})
Copy link
Member

Choose a reason for hiding this comment

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

Nice job preventing accidental double-clicks!

Comment on lines +120 to +136
let updateSalts
if (saltUpdateToken) {
// If we've got a salt update token (i.e., a password change), fetch
// the username associated to the contract to see if they match, and
// then validate the token
const name = request.headers['shelter-name']
const namedContractID = name && await sbp('backend/db/lookupName', name)
if (namedContractID !== deserializedHEAD.contractID) {
throw new Error('Mismatched contract ID and name')
}
updateSalts = await redeemSaltUpdateToken(name, saltUpdateToken)
}
await sbp('backend/server/handleEntry', deserializedHEAD, request.payload)
// If it's a salt update, do it now after handling the message. This way
// we make it less likely that someone will end up locked out from their
// identity contract.
await updateSalts?.(deserializedHEAD.hash)
Copy link
Member

Choose a reason for hiding this comment

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

Claude:

There's a potential race condition between validating the token and applying the update. If multiple requests come in with the same token, the validation could succeed for both but only one should be allowed to update. The token validation and update should be atomic.

Is he full of 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.

Potentially it could happen, however, not really, because the token only applies to one contract and we can't write to the same contract simultaneously (or shouldn't be able to), so handleEntry will throw.

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.

Amazing!

@taoeffect taoeffect merged commit 2932ea3 into master Dec 18, 2024
4 checks passed
@taoeffect taoeffect deleted the feature/password-changing branch December 18, 2024 19:56
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.

Implement updating / changing password
2 participants