-
Notifications
You must be signed in to change notification settings - Fork 25
/
utils.ts
343 lines (288 loc) · 10.5 KB
/
utils.ts
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
import { unmarshalPublicKey } from '@libp2p/crypto/keys'
import { isPeerId } from '@libp2p/interface'
import { logger } from '@libp2p/logger'
import { peerIdFromBytes, peerIdFromKeys } from '@libp2p/peer-id'
import * as cborg from 'cborg'
import errCode from 'err-code'
import { base36 } from 'multiformats/bases/base36'
import { CID } from 'multiformats/cid'
import NanoDate from 'timestamp-nano'
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import * as ERRORS from './errors.js'
import { IpnsEntry } from './pb/ipns.js'
import type { IPNSRecord, IPNSRecordV2, IPNSRecordData } from './index.js'
import type { PublicKey, PeerId } from '@libp2p/interface'
const log = logger('ipns:utils')
const IPNS_PREFIX = uint8ArrayFromString('/ipns/')
const LIBP2P_CID_CODEC = 114
/**
* Convert a JavaScript date into an `RFC3339Nano` formatted
* string
*/
export function toRFC3339 (time: Date): string {
const year = time.getUTCFullYear()
const month = String(time.getUTCMonth() + 1).padStart(2, '0')
const day = String(time.getUTCDate()).padStart(2, '0')
const hour = String(time.getUTCHours()).padStart(2, '0')
const minute = String(time.getUTCMinutes()).padStart(2, '0')
const seconds = String(time.getUTCSeconds()).padStart(2, '0')
const milliseconds = time.getUTCMilliseconds()
const nanoseconds = milliseconds * 1000 * 1000
return `${year}-${month}-${day}T${hour}:${minute}:${seconds}.${nanoseconds}Z`
}
/**
* Parses a date string formatted as `RFC3339Nano` into a
* JavaScript Date object
*/
export function parseRFC3339 (time: string): Date {
const rfc3339Matcher = new RegExp(
// 2006-01-02T
'(\\d{4})-(\\d{2})-(\\d{2})T' +
// 15:04:05
'(\\d{2}):(\\d{2}):(\\d{2})' +
// .999999999Z
'\\.(\\d+)Z'
)
const m = String(time).trim().match(rfc3339Matcher)
if (m == null) {
throw new Error('Invalid format')
}
const year = parseInt(m[1], 10)
const month = parseInt(m[2], 10) - 1
const date = parseInt(m[3], 10)
const hour = parseInt(m[4], 10)
const minute = parseInt(m[5], 10)
const second = parseInt(m[6], 10)
const millisecond = parseInt(m[7].padEnd(6, '0').slice(0, 3), 10)
return new Date(Date.UTC(year, month, date, hour, minute, second, millisecond))
}
/**
* Extracts a public key from the passed PeerId, falling
* back to the pubKey embedded in the ipns record
*/
export const extractPublicKey = async (peerId: PeerId, record: IPNSRecord | IPNSRecordV2): Promise<PublicKey> => {
if (record == null || peerId == null) {
const error = new Error('one or more of the provided parameters are not defined')
log.error(error)
throw errCode(error, ERRORS.ERR_UNDEFINED_PARAMETER)
}
let pubKey: PublicKey | undefined
if (record.pubKey != null) {
try {
pubKey = unmarshalPublicKey(record.pubKey)
} catch (err) {
log.error(err)
throw err
}
const otherId = await peerIdFromKeys(record.pubKey)
if (!otherId.equals(peerId)) {
throw errCode(new Error('Embedded public key did not match PeerID'), ERRORS.ERR_INVALID_EMBEDDED_KEY)
}
} else if (peerId.publicKey != null) {
pubKey = unmarshalPublicKey(peerId.publicKey)
}
if (pubKey != null) {
return pubKey
}
throw errCode(new Error('no public key is available'), ERRORS.ERR_UNDEFINED_PARAMETER)
}
/**
* Utility for creating the record data for being signed
*/
export const ipnsRecordDataForV1Sig = (value: Uint8Array, validityType: IpnsEntry.ValidityType, validity: Uint8Array): Uint8Array => {
const validityTypeBuffer = uint8ArrayFromString(validityType)
return uint8ArrayConcat([value, validity, validityTypeBuffer])
}
/**
* Utility for creating the record data for being signed
*/
export const ipnsRecordDataForV2Sig = (data: Uint8Array): Uint8Array => {
const entryData = uint8ArrayFromString('ipns-signature:')
return uint8ArrayConcat([entryData, data])
}
export const marshal = (obj: IPNSRecord | IPNSRecordV2): Uint8Array => {
if ('signatureV1' in obj) {
return IpnsEntry.encode({
value: uint8ArrayFromString(obj.value),
signatureV1: obj.signatureV1,
validityType: obj.validityType,
validity: uint8ArrayFromString(obj.validity.toString()),
sequence: obj.sequence,
ttl: obj.ttl,
pubKey: obj.pubKey,
signatureV2: obj.signatureV2,
data: obj.data
})
} else {
return IpnsEntry.encode({
pubKey: obj.pubKey,
signatureV2: obj.signatureV2,
data: obj.data
})
}
}
export function unmarshal (buf: Uint8Array): IPNSRecord {
const message = IpnsEntry.decode(buf)
// protobufjs returns bigints as numbers
if (message.sequence != null) {
message.sequence = BigInt(message.sequence)
}
// protobufjs returns bigints as numbers
if (message.ttl != null) {
message.ttl = BigInt(message.ttl)
}
// Check if we have the data field. If we don't, we fail. We've been producing
// V1+V2 records for quite a while and we don't support V1-only records during
// validation any more
if (message.signatureV2 == null || message.data == null) {
throw errCode(new Error('missing data or signatureV2'), ERRORS.ERR_SIGNATURE_VERIFICATION)
}
const data = parseCborData(message.data)
const value = normalizeValue(data.Value)
let validity
try {
validity = NanoDate.fromDate(parseRFC3339(uint8ArrayToString(data.Validity)))
} catch (e) {
log.error('unrecognized validity format (not an rfc3339 format)')
throw errCode(new Error('unrecognized validity format (not an rfc3339 format)'), ERRORS.ERR_UNRECOGNIZED_FORMAT)
}
if (message.value != null && message.signatureV1 != null) {
// V1+V2
validateCborDataMatchesPbData(message)
return {
value,
validityType: IpnsEntry.ValidityType.EOL,
validity,
sequence: data.Sequence,
ttl: data.TTL,
pubKey: message.pubKey,
signatureV1: message.signatureV1,
signatureV2: message.signatureV2,
data: message.data
}
} else if (message.signatureV2 != null) {
// V2-only
return {
value,
validityType: IpnsEntry.ValidityType.EOL,
validity,
sequence: data.Sequence,
ttl: data.TTL,
pubKey: message.pubKey,
signatureV2: message.signatureV2,
data: message.data
}
} else {
throw new Error('invalid record: does not include signatureV1 or signatureV2')
}
}
export const peerIdToRoutingKey = (peerId: PeerId): Uint8Array => {
return uint8ArrayConcat([
IPNS_PREFIX,
peerId.toBytes()
])
}
export const peerIdFromRoutingKey = (key: Uint8Array): PeerId => {
return peerIdFromBytes(key.slice(IPNS_PREFIX.length))
}
export const createCborData = (value: Uint8Array, validity: Uint8Array, validityType: string, sequence: bigint, ttl: bigint): Uint8Array => {
let ValidityType
if (validityType === IpnsEntry.ValidityType.EOL) {
ValidityType = 0
} else {
throw errCode(new Error('Unknown validity type'), ERRORS.ERR_UNRECOGNIZED_VALIDITY)
}
const data = {
Value: value,
Validity: validity,
ValidityType,
Sequence: sequence,
TTL: ttl
}
return cborg.encode(data)
}
export const parseCborData = (buf: Uint8Array): IPNSRecordData => {
const data = cborg.decode(buf)
if (data.ValidityType === 0) {
data.ValidityType = IpnsEntry.ValidityType.EOL
} else {
throw errCode(new Error('Unknown validity type'), ERRORS.ERR_UNRECOGNIZED_VALIDITY)
}
if (Number.isInteger(data.Sequence)) {
// sequence must be a BigInt, but DAG-CBOR doesn't preserve this for Numbers within the safe-integer range
data.Sequence = BigInt(data.Sequence)
}
if (Number.isInteger(data.TTL)) {
// ttl must be a BigInt, but DAG-CBOR doesn't preserve this for Numbers within the safe-integer range
data.TTL = BigInt(data.TTL)
}
return data
}
/**
* Normalizes the given record value. It ensures it is a PeerID, a CID or a
* string starting with '/'. PeerIDs become `/ipns/${cidV1Libp2pKey}`,
* CIDs become `/ipfs/${cidAsV1}`.
*/
export const normalizeValue = (value?: CID | PeerId | string | Uint8Array): string => {
if (value != null) {
// if we have a PeerId, turn it into an ipns path
if (isPeerId(value)) {
return `/ipns/${value.toCID().toString(base36)}`
}
// if the value is bytes, stringify it and see if we have a path
if (value instanceof Uint8Array) {
const string = uint8ArrayToString(value)
if (string.startsWith('/')) {
value = string
}
}
// if we have a path, check it is a valid path
const string = value.toString().trim()
if (string.startsWith('/') && string.length > 1) {
return string
}
// if we have a CID, turn it into an ipfs path
const cid = CID.asCID(value)
if (cid != null) {
// PeerID encoded as a CID
if (cid.code === LIBP2P_CID_CODEC) {
return `/ipns/${cid.toString(base36)}`
}
return `/ipfs/${cid.toV1().toString()}`
}
// try parsing what we have as CID bytes or a CID string
try {
if (value instanceof Uint8Array) {
return `/ipfs/${CID.decode(value).toV1().toString()}`
}
return `/ipfs/${CID.parse(string).toV1().toString()}`
} catch {
// fall through
}
}
throw errCode(new Error('Value must be a valid content path starting with /'), ERRORS.ERR_INVALID_VALUE)
}
const validateCborDataMatchesPbData = (entry: IpnsEntry): void => {
if (entry.data == null) {
throw errCode(new Error('Record data is missing'), ERRORS.ERR_INVALID_RECORD_DATA)
}
const data = parseCborData(entry.data)
if (!uint8ArrayEquals(data.Value, entry.value ?? new Uint8Array(0))) {
throw errCode(new Error('Field "value" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION)
}
if (!uint8ArrayEquals(data.Validity, entry.validity ?? new Uint8Array(0))) {
throw errCode(new Error('Field "validity" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION)
}
if (data.ValidityType !== entry.validityType) {
throw errCode(new Error('Field "validityType" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION)
}
if (data.Sequence !== entry.sequence) {
throw errCode(new Error('Field "sequence" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION)
}
if (data.TTL !== entry.ttl) {
throw errCode(new Error('Field "ttl" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION)
}
}