1
1
import sbp from '@sbp/sbp'
2
2
import { randomBytes , timingSafeEqual } from 'crypto'
3
3
import nacl from 'tweetnacl'
4
- import { base64ToBase64url , base64urlToBase64 , boxKeyPair , computeCAndHc , encryptContractSalt , hash , hashRawStringArray , hashStringArray , parseRegisterSalt , randomNonce } from '~/shared/zkpp.js'
4
+ import { base64ToBase64url , base64urlToBase64 , boxKeyPair , computeCAndHc , decryptSaltUpdate , encryptContractSalt , encryptSaltUpdate , hash , hashRawStringArray , hashStringArray , parseRegisterSalt , randomNonce } from '~/shared/zkpp.js'
5
+ import { AUTHSALT , CONTRACTSALT , SALT_LENGTH_IN_OCTETS , SU } from '~/shared/zkppConstants.js'
5
6
6
7
// used to encrypt salts in database
7
8
let recordSecret : string
8
9
// corresponds to the key for the keyed Hash function in "Log in / session establishment"
9
10
let challengeSecret : string
10
11
// corresponds to a component of s in Step 3 of "Salt registration"
11
12
let registrationSecret : string
13
+ // used to encrypt a stateless token for atomic hash updates
14
+ let hashUpdateSecret : string
12
15
13
16
// Input keying material used to derive various secret keys used in this
14
17
// protocol: recordSecret, challengeSecret and registrationSecret.
@@ -60,10 +63,23 @@ export const initZkpp = async () => {
60
63
recordSecret = Buffer . from ( hashStringArray ( 'private/recordSecret' , IKM ) ) . toString ( 'base64' )
61
64
challengeSecret = Buffer . from ( hashStringArray ( 'private/challengeSecret' , IKM ) ) . toString ( 'base64' )
62
65
registrationSecret = Buffer . from ( hashStringArray ( 'private/registrationSecret' , IKM ) ) . toString ( 'base64' )
66
+ hashUpdateSecret = Buffer . from ( hashStringArray ( 'private/hashUpdateSecret' , IKM ) ) . toString ( 'base64' )
63
67
}
64
68
65
69
const maxAge = 30
66
70
71
+ const computeZkppSaltRecordId = async ( contractID : string ) => {
72
+ const recordId = `_private_rid_${ contractID } `
73
+ const record = await sbp ( 'chelonia/db/get' , recordId )
74
+
75
+ if ( ! record ) {
76
+ return null
77
+ }
78
+
79
+ const recordBuf = Buffer . concat ( [ Buffer . from ( contractID ) , Buffer . from ( record ) ] )
80
+ return hash ( recordBuf )
81
+ }
82
+
67
83
const getZkppSaltRecord = async ( contractID : string ) => {
68
84
const recordId = `_private_rid_${ contractID } `
69
85
const record = await sbp ( 'chelonia/db/get' , recordId )
@@ -85,17 +101,23 @@ const getZkppSaltRecord = async (contractID: string) => {
85
101
try {
86
102
const recordObj = JSON . parse ( recordString )
87
103
88
- if ( ! Array . isArray ( recordObj ) || recordObj . length !== 3 || ! recordObj . reduce ( ( acc , cv ) => acc && typeof cv === 'string' , true ) ) {
104
+ if (
105
+ ! Array . isArray ( recordObj ) ||
106
+ ( recordObj . length !== 3 && recordObj . length !== 4 ) ||
107
+ recordObj . slice ( 0 , 3 ) . some ( ( r ) => ! r || typeof r !== 'string' ) ||
108
+ ( recordObj [ 3 ] !== null && typeof recordObj [ 3 ] !== 'string' )
109
+ ) {
89
110
console . error ( 'Error validating encrypted JSON object ' + recordId )
90
111
return null
91
112
}
92
113
93
- const [ hashedPassword , authSalt , contractSalt ] = recordObj
114
+ const [ hashedPassword , authSalt , contractSalt , cid ] = recordObj
94
115
95
116
return {
96
117
hashedPassword,
97
118
authSalt,
98
- contractSalt
119
+ contractSalt,
120
+ cid
99
121
}
100
122
} catch {
101
123
console . error ( 'Error parsing encrypted JSON object ' + recordId )
@@ -105,11 +127,11 @@ const getZkppSaltRecord = async (contractID: string) => {
105
127
return null
106
128
}
107
129
108
- const setZkppSaltRecord = async ( contractID : string , hashedPassword : string , authSalt : string , contractSalt : string ) => {
130
+ const setZkppSaltRecord = async ( contractID : string , hashedPassword : string , authSalt : string , contractSalt : string , cid : ? string ) => {
109
131
const recordId = `_private_rid_${ contractID } `
110
132
const encryptionKey = hashStringArray ( 'REK' , contractID , recordSecret ) . slice ( 0 , nacl . secretbox . keyLength )
111
133
const nonce = nacl . randomBytes ( nacl . secretbox . nonceLength )
112
- const recordPlaintext = JSON . stringify ( [ hashedPassword , authSalt , contractSalt ] )
134
+ const recordPlaintext = JSON . stringify ( [ hashedPassword , authSalt , contractSalt , cid ] )
113
135
const recordCiphertext = nacl . secretbox ( Buffer . from ( recordPlaintext ) , nonce , encryptionKey )
114
136
const recordBuf = Buffer . concat ( [ nonce , recordCiphertext ] )
115
137
const record = base64ToBase64url ( recordBuf . toString ( 'base64' ) )
@@ -242,7 +264,7 @@ export const getContractSalt = async (contract: string, r: string, s: string, si
242
264
return false
243
265
}
244
266
245
- const { hashedPassword, contractSalt } = record
267
+ const { hashedPassword, contractSalt, cid } = record
246
268
247
269
const c = contractSaltVerifyC ( hashedPassword , r , s , hc )
248
270
@@ -251,10 +273,10 @@ export const getContractSalt = async (contract: string, r: string, s: string, si
251
273
throw new Error ( 'getContractSalt: Bad challenge' )
252
274
}
253
275
254
- return encryptContractSalt ( c , contractSalt )
276
+ return encryptContractSalt ( c , JSON . stringify ( [ contractSalt , cid ] ) )
255
277
}
256
278
257
- export const updateContractSalt = async ( contract : string , r : string , s : string , sig : string , hc : string , encryptedArgs : string ) : Promise < boolean > => {
279
+ export const updateContractSalt = async ( contract : string , r : string , s : string , sig : string , hc : string , encryptedArgs : string ) : Promise < boolean | string > => {
258
280
if ( ! verifyChallenge ( contract , r , s , sig ) ) {
259
281
console . warn ( 'update: Error validating challenge: ' + JSON . stringify ( { contract, r, s, sig } ) )
260
282
throw new Error ( 'update: Bad challenge' )
@@ -266,7 +288,7 @@ export const updateContractSalt = async (contract: string, r: string, s: string,
266
288
console . error ( 'update: Error obtaining ZKPP salt record for contract ID ' + contract )
267
289
return false
268
290
}
269
- const { hashedPassword } = record
291
+ const { hashedPassword, contractSalt : oldContractSalt } = record
270
292
271
293
const c = contractSaltVerifyC ( hashedPassword , r , s , hc )
272
294
@@ -275,7 +297,7 @@ export const updateContractSalt = async (contract: string, r: string, s: string,
275
297
throw new Error ( 'update: Bad challenge' )
276
298
}
277
299
278
- const encryptionKey = hashRawStringArray ( 'SU' , c ) . slice ( 0 , nacl . secretbox . keyLength )
300
+ const encryptionKey = hashRawStringArray ( SU , c ) . slice ( 0 , nacl . secretbox . keyLength )
279
301
const encryptedArgsBuf = Buffer . from ( base64urlToBase64 ( encryptedArgs ) , 'base64' )
280
302
const nonce = encryptedArgsBuf . slice ( 0 , nacl . secretbox . nonceLength )
281
303
const encryptedArgsCiphertext = encryptedArgsBuf . slice ( nacl . secretbox . nonceLength )
@@ -288,21 +310,50 @@ export const updateContractSalt = async (contract: string, r: string, s: string,
288
310
}
289
311
290
312
try {
291
- const argsObj = JSON . parse ( Buffer . from ( args ) . toString ( ) )
313
+ const hashedPassword = Buffer . from ( args ) . toString ( )
292
314
293
- if ( ! Array . isArray ( argsObj ) || argsObj . length !== 3 || ! argsObj . reduce ( ( acc , cv ) => acc && typeof cv === 'string' , true ) ) {
294
- console . error ( `update: Error validating the encrypted arguments for contract ID ${ contract } (${ JSON . stringify ( { r, s, hc } ) } )` )
315
+ const recordId = await computeZkppSaltRecordId ( contract )
316
+ if ( ! recordId ) {
317
+ console . error ( `update: Error obtaining record ID for contract ID ${ contract } ` )
295
318
return false
296
319
}
297
320
298
- const [ hashedPassword , authSalt , contractSalt ] = argsObj
321
+ const authSalt = Buffer . from ( hashStringArray ( AUTHSALT , c ) ) . slice ( 0 , SALT_LENGTH_IN_OCTETS ) . toString ( 'base64' )
322
+ const contractSalt = Buffer . from ( hashStringArray ( CONTRACTSALT , c ) ) . slice ( 0 , SALT_LENGTH_IN_OCTETS ) . toString ( 'base64' )
299
323
300
- await setZkppSaltRecord ( contract , hashedPassword , authSalt , contractSalt )
324
+ const token = encryptSaltUpdate (
325
+ hashUpdateSecret ,
326
+ recordId ,
327
+ JSON . stringify ( [ Date . now ( ) , hashedPassword , authSalt , contractSalt ] )
328
+ )
301
329
302
- return true
330
+ return encryptContractSalt ( c , JSON . stringify ( [ oldContractSalt , token ] ) )
303
331
} catch {
304
332
console . error ( `update: Error parsing encrypted arguments for contract ID ${ contract } (${ JSON . stringify ( { r, s, hc } ) } )` )
305
333
}
306
334
307
335
return false
308
336
}
337
+
338
+ export const redeemSaltUpdateToken = async ( contract : string , token : string ) : Promise < ( cid : ?string ) => Promise < void >> => {
339
+ const recordId = await computeZkppSaltRecordId ( contract )
340
+ if ( ! recordId ) {
341
+ throw new Error ( 'Record ID not found' )
342
+ }
343
+
344
+ const decryptedToken = decryptSaltUpdate (
345
+ hashUpdateSecret ,
346
+ recordId ,
347
+ token
348
+ )
349
+
350
+ const [ timestamp , hashedPassword , authSalt , contractSalt ] = JSON . parse ( decryptedToken )
351
+
352
+ if ( timestamp < ( Date . now ( ) - 180e3 ) ) {
353
+ throw new Error ( 'ZKPP token expired' )
354
+ }
355
+
356
+ return ( cid : ?string ) => {
357
+ return setZkppSaltRecord ( contract , hashedPassword , authSalt , contractSalt , cid )
358
+ }
359
+ }
0 commit comments