-
Notifications
You must be signed in to change notification settings - Fork 25
/
index.js
348 lines (299 loc) · 11.3 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
'use strict'
const base32Encode = require('base32-encode')
const NanoDate = require('timestamp-nano')
const { Key } = require('interface-datastore')
const crypto = require('libp2p-crypto')
const PeerId = require('peer-id')
const multihash = require('multihashes')
const debug = require('debug')
const log = debug('jsipns')
log.error = debug('jsipns:error')
const ipnsEntryProto = require('./pb/ipns.proto')
const { parseRFC3339 } = require('./utils')
const ERRORS = require('./errors')
const ID_MULTIHASH_CODE = multihash.names.id
const namespace = '/ipns/'
/**
* Creates a new ipns entry and signs it with the given private key.
* The ipns entry validity should follow the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision.
* Note: This function does not embed the public key. If you want to do that, use `EmbedPublicKey`.
*
* @param {Object} privateKey private key for signing the record.
* @param {string} value value to be stored in the record.
* @param {number} seq number representing the current version of the record.
* @param {number|string} lifetime lifetime of the record (in milliseconds).
* @param {function(Error, entry)} [callback]
*/
const create = (privateKey, value, seq, lifetime, callback) => {
// Validity in ISOString with nanoseconds precision and validity type EOL
const isoValidity = new NanoDate(Date.now() + Number(lifetime)).toString()
const validityType = ipnsEntryProto.ValidityType.EOL
_create(privateKey, value, seq, isoValidity, validityType, callback)
}
/**
* Same as create(), but instead of generating a new Date, it receives the intended expiration time
* WARNING: nano precision is not standard, make sure the value in seconds is 9 orders of magnitude lesser than the one provided.
* @param {Object} privateKey private key for signing the record.
* @param {string} value value to be stored in the record.
* @param {number} seq number representing the current version of the record.
* @param {string} expiration expiration datetime for record in the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision.
* @param {function(Error, entry)} [callback]
*/
const createWithExpiration = (privateKey, value, seq, expiration, callback) => {
const validityType = ipnsEntryProto.ValidityType.EOL
_create(privateKey, value, seq, expiration, validityType, callback)
}
const _create = (privateKey, value, seq, isoValidity, validityType, callback) => {
sign(privateKey, value, validityType, isoValidity, (error, signature) => {
if (error) {
log.error('record signature creation failed')
return callback(Object.assign(new Error('record signature verification failed'), { code: ERRORS.ERR_SIGNATURE_CREATION }))
}
const entry = {
value: value,
signature: signature,
validityType: validityType,
validity: isoValidity,
sequence: seq
}
log(`ipns entry for ${value} created`)
return callback(null, entry)
})
}
/**
* Validates the given ipns entry against the given public key.
*
* @param {Object} publicKey public key for validating the record.
* @param {Object} entry ipns entry record.
* @param {function(Error)} [callback]
*/
const validate = (publicKey, entry, callback) => {
const { value, validityType, validity } = entry
const dataForSignature = ipnsEntryDataForSig(value, validityType, validity)
// Validate Signature
publicKey.verify(dataForSignature, entry.signature, (err, isValid) => {
if (err || !isValid) {
log.error('record signature verification failed')
return callback(Object.assign(new Error('record signature verification failed'), { code: ERRORS.ERR_SIGNATURE_VERIFICATION }))
}
// Validate according to the validity type
if (validityType === ipnsEntryProto.ValidityType.EOL) {
let validityDate
try {
validityDate = parseRFC3339(validity.toString())
} catch (e) {
log.error('unrecognized validity format (not an rfc3339 format)')
return callback(Object.assign(new Error('unrecognized validity format (not an rfc3339 format)'), { code: ERRORS.ERR_UNRECOGNIZED_FORMAT }))
}
if (validityDate < Date.now()) {
log.error('record has expired')
return callback(Object.assign(new Error('record has expired'), { code: ERRORS.ERR_IPNS_EXPIRED_RECORD }))
}
} else if (validityType) {
log.error('unrecognized validity type')
return callback(Object.assign(new Error('unrecognized validity type'), { code: ERRORS.ERR_UNRECOGNIZED_VALIDITY }))
}
log(`ipns entry for ${value} is valid`)
return callback(null, null)
})
}
/**
* Embed the given public key in the given entry. While not strictly required,
* some nodes (eg. DHT servers) may reject IPNS entries that don't embed their
* public keys as they may not be able to validate them efficiently.
* As a consequence of nodes needing to validade a record upon receipt, they need
* the public key associated with it. For olde RSA keys, it is easier if we just
* send this as part of the record itself. For newer ed25519 keys, the public key
* can be embedded in the peerId.
*
* @param {Object} publicKey public key to embed.
* @param {Object} entry ipns entry record.
* @param {function(Error)} [callback]
* @return {Void}
*/
const embedPublicKey = (publicKey, entry, callback) => {
if (!publicKey || !publicKey.bytes || !entry) {
const error = 'one or more of the provided parameters are not defined'
log.error(error)
return callback(Object.assign(new Error(error), { code: ERRORS.ERR_UNDEFINED_PARAMETER }))
}
// Create a peer id from the public key.
PeerId.createFromPubKey(publicKey.bytes, (err, peerId) => {
if (err) {
log.error(err)
return callback(Object.assign(new Error(err), { code: ERRORS.ERR_PEER_ID_FROM_PUBLIC_KEY }))
}
// Try to extract the public key from the ID. If we can, no need to embed it
let extractedPublicKey
try {
extractedPublicKey = extractPublicKeyFromId(peerId)
} catch (err) {
log.error(err)
return callback(Object.assign(new Error(err), { code: ERRORS.ERR_PUBLIC_KEY_FROM_ID }))
}
if (extractedPublicKey) {
return callback(null, null)
}
// If we failed to extract the public key from the peer ID, embed it in the record.
try {
entry.pubKey = crypto.keys.marshalPublicKey(publicKey)
} catch (err) {
log.error(err)
return callback(err)
}
callback(null, entry)
})
}
/**
* Extracts a public key matching `pid` from the ipns record.
*
* @param {Object} peerId peer identifier object.
* @param {Object} entry ipns entry record.
* @param {function(Error)} [callback]
* @return {Void}
*/
const extractPublicKey = (peerId, entry, callback) => {
if (!entry || !peerId) {
const error = 'one or more of the provided parameters are not defined'
log.error(error)
return callback(Object.assign(new Error(error), { code: ERRORS.ERR_UNDEFINED_PARAMETER }))
}
if (entry.pubKey) {
let pubKey
try {
pubKey = crypto.keys.unmarshalPublicKey(entry.pubKey)
} catch (err) {
log.error(err)
return callback(err)
}
return callback(null, pubKey)
}
if (peerId.pubKey) {
callback(null, peerId.pubKey)
} else {
callback(Object.assign(new Error('no public key is available'), { code: ERRORS.ERR_UNDEFINED_PARAMETER }))
}
}
// rawStdEncoding with RFC4648
const rawStdEncoding = (key) => base32Encode(key, 'RFC4648', { padding: false })
/**
* Get key for storing the record locally.
* Format: /ipns/${base32(<HASH>)}
*
* @param {Buffer} key peer identifier object.
* @returns {string}
*/
const getLocalKey = (key) => new Key(`/ipns/${rawStdEncoding(key)}`)
/**
* Get key for sharing the record in the routing mechanism.
* Format: ${base32(/ipns/<HASH>)}, ${base32(/pk/<HASH>)}
*
* @param {Buffer} pid peer identifier represented by the multihash of the public key as Buffer.
* @returns {Object} containing the `nameKey` and the `ipnsKey`.
*/
const getIdKeys = (pid) => {
const pkBuffer = Buffer.from('/pk/')
const ipnsBuffer = Buffer.from('/ipns/')
return {
routingPubKey: new Key(Buffer.concat([pkBuffer, pid])), // Added on https://github.com/ipfs/js-ipns/pull/8#issue-213857876 (pkKey will be deprecated in a future release)
pkKey: new Key(rawStdEncoding(Buffer.concat([pkBuffer, pid]))),
routingKey: new Key(Buffer.concat([ipnsBuffer, pid])), // Added on https://github.com/ipfs/js-ipns/pull/6#issue-213631461 (ipnsKey will be deprecated in a future release)
ipnsKey: new Key(rawStdEncoding(Buffer.concat([ipnsBuffer, pid])))
}
}
// Sign ipns record data
const sign = (privateKey, value, validityType, validity, callback) => {
const dataForSignature = ipnsEntryDataForSig(value, validityType, validity)
privateKey.sign(dataForSignature, (err, signature) => {
if (err) {
return callback(err)
}
return callback(null, signature)
})
}
// Utility for getting the validity type code name of a validity
const getValidityType = (validityType) => {
if (validityType.toString() === '0') {
return 'EOL'
} else {
const error = `unrecognized validity type ${validityType.toString()}`
log.error(error)
throw Object.assign(new Error(error), { code: ERRORS.ERR_UNRECOGNIZED_VALIDITY })
}
}
// Utility for creating the record data for being signed
const ipnsEntryDataForSig = (value, validityType, validity) => {
const valueBuffer = Buffer.from(value)
const validityTypeBuffer = Buffer.from(getValidityType(validityType))
const validityBuffer = Buffer.from(validity)
return Buffer.concat([valueBuffer, validityBuffer, validityTypeBuffer])
}
// Utility for extracting the public key from a peer-id
const extractPublicKeyFromId = (peerId) => {
const decodedId = multihash.decode(peerId.id)
if (decodedId.code !== ID_MULTIHASH_CODE) {
return null
}
return crypto.keys.unmarshalPublicKey(decodedId.digest)
}
const marshal = ipnsEntryProto.encode
const unmarshal = ipnsEntryProto.decode
const validator = {
validate: (marshalledData, key, callback) => {
const receivedEntry = unmarshal(marshalledData)
const bufferId = key.slice('/ipns/'.length)
let peerId
try {
peerId = PeerId.createFromBytes(bufferId)
} catch (err) {
return callback(err)
}
// extract public key
extractPublicKey(peerId, receivedEntry, (err, pubKey) => {
if (err) {
return callback(err)
}
// Record validation
validate(pubKey, receivedEntry, (err) => {
if (err) {
return callback(err)
}
callback(null, true)
})
})
},
select: (dataA, dataB, callback) => {
const entryA = unmarshal(dataA)
const entryB = unmarshal(dataB)
const index = entryA.sequence > entryB.sequence ? 0 : 1
if (typeof callback !== 'function') {
return index
}
callback(null, index)
}
}
module.exports = {
// create ipns entry record
create,
// create ipns entry record specifying the expiration time
createWithExpiration,
// validate ipns entry record
validate,
// embed public key in the record
embedPublicKey,
// extract public key from the record
extractPublicKey,
// get key for storing the entry locally
getLocalKey,
// get keys for routing
getIdKeys,
// marshal
marshal,
// unmarshal
unmarshal,
// validator
validator,
// namespace
namespace,
namespaceLength: namespace.length
}