@@ -10,6 +10,9 @@ const multibase = require('multibase')
10
10
const uint8ArrayFromString = require ( 'uint8arrays/from-string' )
11
11
const uint8ArrayToString = require ( 'uint8arrays/to-string' )
12
12
const uint8ArrayConcat = require ( 'uint8arrays/concat' )
13
+ const uint8ArrayEquals = require ( 'uint8arrays/equals' )
14
+ const cborg = require ( 'cborg' )
15
+ const Long = require ( 'long' )
13
16
14
17
const debug = require ( 'debug' )
15
18
const log = Object . assign ( debug ( 'jsipns' ) , {
@@ -39,14 +42,17 @@ const namespace = '/ipns/'
39
42
*
40
43
* @param {PrivateKey } privateKey - private key for signing the record.
41
44
* @param {Uint8Array } value - value to be stored in the record.
42
- * @param {number } seq - number representing the current version of the record.
45
+ * @param {number | bigint } seq - number representing the current version of the record.
43
46
* @param {number } lifetime - lifetime of the record (in milliseconds).
44
47
*/
45
48
const create = ( privateKey , value , seq , lifetime ) => {
46
49
// Validity in ISOString with nanoseconds precision and validity type EOL
47
- const isoValidity = new NanoDate ( Date . now ( ) + Number ( lifetime ) ) . toString ( )
50
+ const expirationDate = new NanoDate ( Date . now ( ) + Number ( lifetime ) )
48
51
const validityType = ipnsEntryProto . ValidityType . EOL
49
- return _create ( privateKey , value , seq , uint8ArrayFromString ( isoValidity ) , validityType )
52
+ const [ ms , ns ] = lifetime . toString ( ) . split ( '.' )
53
+ const lifetimeNs = BigInt ( ms ) * 100000n + BigInt ( ns || 0 )
54
+
55
+ return _create ( privateKey , value , seq , validityType , expirationDate , lifetimeNs )
50
56
}
51
57
52
58
/**
@@ -55,36 +61,69 @@ const create = (privateKey, value, seq, lifetime) => {
55
61
*
56
62
* @param {PrivateKey } privateKey - private key for signing the record.
57
63
* @param {Uint8Array } value - value to be stored in the record.
58
- * @param {number } seq - number representing the current version of the record.
64
+ * @param {number | bigint } seq - number representing the current version of the record.
59
65
* @param {string } expiration - expiration datetime for record in the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision.
60
66
*/
61
67
const createWithExpiration = ( privateKey , value , seq , expiration ) => {
68
+ const expirationDate = NanoDate . fromString ( expiration )
62
69
const validityType = ipnsEntryProto . ValidityType . EOL
63
- return _create ( privateKey , value , seq , uint8ArrayFromString ( expiration ) , validityType )
70
+
71
+ const ttlMs = expirationDate . toDate ( ) . getTime ( ) - Date . now ( )
72
+ const ttlNs = ( BigInt ( ttlMs ) * 100000n ) + BigInt ( expirationDate . getNano ( ) )
73
+
74
+ return _create ( privateKey , value , seq , validityType , expirationDate , ttlNs )
64
75
}
65
76
66
77
/**
67
78
* @param {PrivateKey } privateKey
68
79
* @param {Uint8Array } value
69
- * @param {number } seq
70
- * @param {Uint8Array } isoValidity
80
+ * @param {number | bigint } seq
71
81
* @param {number } validityType
82
+ * @param {NanoDate } expirationDate
83
+ * @param {bigint } ttl
72
84
*/
73
- const _create = async ( privateKey , value , seq , isoValidity , validityType ) => {
74
- const signature = await sign ( privateKey , value , validityType , isoValidity )
85
+ const _create = async ( privateKey , value , seq , validityType , expirationDate , ttl ) => {
86
+ seq = BigInt ( seq )
87
+ const isoValidity = uint8ArrayFromString ( expirationDate . toString ( ) )
88
+ const signatureV1 = await sign ( privateKey , value , validityType , isoValidity )
89
+ const data = createCborData ( value , isoValidity , validityType , seq , ttl )
90
+ const sigData = ipnsEntryDataForV2Sig ( data )
91
+ const signatureV2 = await privateKey . sign ( sigData )
75
92
76
93
const entry = {
77
94
value,
78
- signature : signature ,
95
+ signature : signatureV1 ,
79
96
validityType : validityType ,
80
97
validity : isoValidity ,
81
- sequence : seq
98
+ sequence : seq ,
99
+ ttl,
100
+ signatureV2,
101
+ data
82
102
}
83
103
84
104
log ( `ipns entry for ${ value } created` )
85
105
return entry
86
106
}
87
107
108
+ /**
109
+ * @param {Uint8Array } value
110
+ * @param {Uint8Array } validity
111
+ * @param {number } validityType
112
+ * @param {bigint } sequence
113
+ * @param {bigint } ttl
114
+ */
115
+ const createCborData = ( value , validity , validityType , sequence , ttl ) => {
116
+ const data = {
117
+ value,
118
+ validity,
119
+ validityType,
120
+ sequence,
121
+ ttl
122
+ }
123
+
124
+ return cborg . encode ( data )
125
+ }
126
+
88
127
/**
89
128
* Validates the given ipns entry against the given public key.
90
129
*
@@ -93,12 +132,26 @@ const _create = async (privateKey, value, seq, isoValidity, validityType) => {
93
132
*/
94
133
const validate = async ( publicKey , entry ) => {
95
134
const { value, validityType, validity } = entry
96
- const dataForSignature = ipnsEntryDataForSig ( value , validityType , validity )
135
+
136
+ /** @type {Uint8Array } */
137
+ let dataForSignature
138
+ let signature
139
+
140
+ // Check v2 signature if it's available, otherwise use the v1 signature
141
+ if ( entry . signatureV2 && entry . data ) {
142
+ signature = entry . signatureV2
143
+ dataForSignature = ipnsEntryDataForV2Sig ( entry . data )
144
+
145
+ validateCborDataMatchesPbData ( entry )
146
+ } else {
147
+ signature = entry . signature
148
+ dataForSignature = ipnsEntryDataForV1Sig ( value , validityType , validity )
149
+ }
97
150
98
151
// Validate Signature
99
152
let isValid
100
153
try {
101
- isValid = await publicKey . verify ( dataForSignature , entry . signature )
154
+ isValid = await publicKey . verify ( dataForSignature , signature )
102
155
} catch ( err ) {
103
156
isValid = false
104
157
}
@@ -130,12 +183,53 @@ const validate = async (publicKey, entry) => {
130
183
log ( `ipns entry for ${ value } is valid` )
131
184
}
132
185
186
+ /**
187
+ * @param {IPNSEntry } entry
188
+ */
189
+ const validateCborDataMatchesPbData = ( entry ) => {
190
+ if ( ! entry . data ) {
191
+ throw errCode ( new Error ( 'Record data is missing' ) , ERRORS . ERR_INVALID_RECORD_DATA )
192
+ }
193
+
194
+ const data = cborg . decode ( entry . data )
195
+
196
+ if ( Number . isInteger ( data . sequence ) ) {
197
+ // sequence must be a BigInt, but DAG-CBOR doesn't preserve this for Numbers within the safe-integer range
198
+ data . sequence = BigInt ( data . sequence )
199
+ }
200
+
201
+ if ( Number . isInteger ( data . ttl ) ) {
202
+ // ttl must be a BigInt, but DAG-CBOR doesn't preserve this for Numbers within the safe-integer range
203
+ data . ttl = BigInt ( data . ttl )
204
+ }
205
+
206
+ if ( ! uint8ArrayEquals ( data . value , entry . value ) ) {
207
+ throw errCode ( new Error ( 'Field "value" did not match between protobuf and CBOR' ) , ERRORS . ERR_SIGNATURE_VERIFICATION )
208
+ }
209
+
210
+ if ( ! uint8ArrayEquals ( data . validity , entry . validity ) ) {
211
+ throw errCode ( new Error ( 'Field "validity" did not match between protobuf and CBOR' ) , ERRORS . ERR_SIGNATURE_VERIFICATION )
212
+ }
213
+
214
+ if ( data . validityType !== entry . validityType ) {
215
+ throw errCode ( new Error ( 'Field "validityType" did not match between protobuf and CBOR' ) , ERRORS . ERR_SIGNATURE_VERIFICATION )
216
+ }
217
+
218
+ if ( data . sequence !== entry . sequence ) {
219
+ throw errCode ( new Error ( 'Field "sequence" did not match between protobuf and CBOR' ) , ERRORS . ERR_SIGNATURE_VERIFICATION )
220
+ }
221
+
222
+ if ( data . ttl !== entry . ttl ) {
223
+ throw errCode ( new Error ( 'Field "ttl" did not match between protobuf and CBOR' ) , ERRORS . ERR_SIGNATURE_VERIFICATION )
224
+ }
225
+ }
226
+
133
227
/**
134
228
* Embed the given public key in the given entry. While not strictly required,
135
229
* some nodes (eg. DHT servers) may reject IPNS entries that don't embed their
136
230
* public keys as they may not be able to validate them efficiently.
137
- * As a consequence of nodes needing to validade a record upon receipt, they need
138
- * the public key associated with it. For olde RSA keys, it is easier if we just
231
+ * As a consequence of nodes needing to validate a record upon receipt, they need
232
+ * the public key associated with it. For old RSA keys, it is easier if we just
139
233
* send this as part of the record itself. For newer ed25519 keys, the public key
140
234
* can be embedded in the peerId.
141
235
*
@@ -254,7 +348,7 @@ const getIdKeys = (pid) => {
254
348
*/
255
349
const sign = ( privateKey , value , validityType , validity ) => {
256
350
try {
257
- const dataForSignature = ipnsEntryDataForSig ( value , validityType , validity )
351
+ const dataForSignature = ipnsEntryDataForV1Sig ( value , validityType , validity )
258
352
259
353
return privateKey . sign ( dataForSignature )
260
354
} catch ( error ) {
@@ -285,12 +379,23 @@ const getValidityType = (validityType) => {
285
379
* @param {number } validityType
286
380
* @param {Uint8Array } validity
287
381
*/
288
- const ipnsEntryDataForSig = ( value , validityType , validity ) => {
382
+ const ipnsEntryDataForV1Sig = ( value , validityType , validity ) => {
289
383
const validityTypeBuffer = uint8ArrayFromString ( getValidityType ( validityType ) )
290
384
291
385
return uint8ArrayConcat ( [ value , validity , validityTypeBuffer ] )
292
386
}
293
387
388
+ /**
389
+ * Utility for creating the record data for being signed
390
+ *
391
+ * @param {Uint8Array } data
392
+ */
393
+ const ipnsEntryDataForV2Sig = ( data ) => {
394
+ const entryData = uint8ArrayFromString ( 'ipns-signature:' )
395
+
396
+ return uint8ArrayConcat ( [ entryData , data ] )
397
+ }
398
+
294
399
/**
295
400
* Utility for extracting the public key from a peer-id
296
401
*
@@ -310,7 +415,11 @@ const extractPublicKeyFromId = (peerId) => {
310
415
* @param {IPNSEntry } obj
311
416
*/
312
417
const marshal = ( obj ) => {
313
- return ipnsEntryProto . encode ( obj ) . finish ( )
418
+ return ipnsEntryProto . encode ( {
419
+ ...obj ,
420
+ sequence : Long . fromString ( obj . sequence . toString ( ) ) ,
421
+ ttl : obj . ttl == null ? undefined : Long . fromString ( obj . ttl . toString ( ) )
422
+ } ) . finish ( )
314
423
}
315
424
316
425
/**
@@ -322,7 +431,6 @@ const unmarshal = (buf) => {
322
431
const object = ipnsEntryProto . toObject ( message , {
323
432
defaults : false ,
324
433
arrays : true ,
325
- longs : Number ,
326
434
objects : false
327
435
} )
328
436
@@ -331,8 +439,9 @@ const unmarshal = (buf) => {
331
439
signature : object . signature ,
332
440
validityType : object . validityType ,
333
441
validity : object . validity ,
334
- sequence : object . sequence ,
335
- pubKey : object . pubKey
442
+ sequence : Object . hasOwnProperty . call ( object , 'sequence' ) ? BigInt ( `${ object . sequence } ` ) : 0n ,
443
+ pubKey : object . pubKey ,
444
+ ttl : Object . hasOwnProperty . call ( object , 'ttl' ) ? BigInt ( `${ object . ttl } ` ) : undefined
336
445
}
337
446
}
338
447
0 commit comments