-
-
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
Password changes #2446
Password changes #2446
Conversation
group-income Run #3597
Run Properties:
|
Project |
group-income
|
Branch Review |
feature/password-changing
|
Run status |
Passed #3597
|
Run duration | 11m 32s |
Commit |
a335b1484b ℹ️: Merge e8f52016cca709df74fd77b186835100933171f6 into 6b1ad586cc42852aabfbc98ef52c...
|
Committer | Ricardo Iván Vieitez Parra |
View all properties for this run ↗︎ |
Test results | |
---|---|
Failures |
0
|
Flaky |
0
|
Pending |
10
|
Skipped |
0
|
Passing |
112
|
View all changes introduced in this branch ↗︎ |
7c5c849
to
6a5a0d5
Compare
b6358a9
to
140c826
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.
Questions...
bb86fa5
to
e4d05fd
Compare
e4d05fd
to
e88c818
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.
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:
- After logging in, change the password.
- Log out
- Verify that logging back in with the old password doesn't work
- Verify that logging back in with the new password does work
I believe this is ready to merge, for the most part. |
@@ -116,7 +116,24 @@ route.POST('/event', { | |||
} | |||
} | |||
} | |||
const saltUpdateToken = request.headers['shelter-salt-update-token'] |
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 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', { |
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.
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') |
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.
Used for encrypting the update token.
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.
Are these strings like 'private/hashUpdateSecret'
something that should be put in zkppConstants.js
too?
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.
Probably not, since they're only used here for initialisation purposes.
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.
They also have no particular significance with regard to the protocol.
} | ||
|
||
return (cid: ?string) => { | ||
return setZkppSaltRecord(contract, hashedPassword, authSalt, contractSalt, cid) |
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.
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. | |||
| {{ ' ' }} |
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 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 |
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.
Explanation you requested added here.
|
||
return true | ||
return encryptContractSalt(c, JSON.stringify([oldContractSalt, token])) |
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.
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) || |
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 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])) |
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 now return the contractSalt and the CID.
export default (sbp('sbp/selectors/register', { | ||
'gi.actions/identity/create': async function ({ | ||
IPK, | ||
IEK, | ||
IPK: wIPK, |
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.
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)) |
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 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)), |
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.
Implement the old TODO.
|
||
const [newContractSalt, oldContractSalt, updateToken] = await sbp('gi.app/identity/updateSaltRequest', username, wOldPassword, wNewPassword) | ||
|
||
const oldIPK = await deriveKeyFromPassword(EDWARDS25519SHA512BATCH, oldPassword, oldContractSalt) |
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 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 }' |
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.
These seemed to be wrong.
:disabled='$v.form.$invalid' | ||
) Change password | ||
|
||
template(slot='errors') {{ form.response }} |
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 didn't work, and the slot doesn't exist. Replaced it with banner-scoped
.
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.
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:
- Create account
u1
in Firefox Dev Edition, with group - Open Safari, attempt to sign in as
u1
Results:
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
})().finally(() => { | ||
this.processing = 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.
Nice job preventing accidental double-clicks!
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) |
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.
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?
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.
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.
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.
Amazing!
Closes #1923