Skip to content
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

fix!: update to [email protected] deps #322

Merged
merged 3 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 0 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
- [Validate record](#validate-record)
- [Embed public key to record](#embed-public-key-to-record)
- [Extract public key from record](#extract-public-key-from-record)
- [Datastore key](#datastore-key)
- [Marshal data with proto buffer](#marshal-data-with-proto-buffer)
- [Unmarshal data from proto buffer](#unmarshal-data-from-proto-buffer)
- [Validator](#validator)
Expand Down Expand Up @@ -82,20 +81,6 @@ import * as ipns from 'ipns'
const publicKey = await ipns.extractPublicKey(peerId, ipnsRecord)
```

### Datastore key

```js
import * as ipns from 'ipns'

ipns.getLocalKey(peerId)
```

Returns a key to be used for storing the IPNS record locally, that is:

```
/ipns/${base32(<HASH>)}
```

### Marshal data with proto buffer

```js
Expand Down
24 changes: 11 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,23 +166,21 @@
"docs:no-publish": "NODE_OPTIONS=--max_old_space_size=8192 aegir docs --publish false"
},
"dependencies": {
"@libp2p/crypto": "^4.0.0",
"@libp2p/interface": "^1.1.0",
"@libp2p/logger": "^4.0.3",
"@libp2p/peer-id": "^4.0.3",
"cborg": "^4.0.1",
"err-code": "^3.0.1",
"interface-datastore": "^8.1.0",
"multiformats": "^13.0.0",
"protons-runtime": "^5.2.1",
"timestamp-nano": "^1.0.0",
"@libp2p/crypto": "^5.0.0",
"@libp2p/interface": "^2.0.0",
"@libp2p/logger": "^5.0.0",
"cborg": "^4.2.3",
"interface-datastore": "^8.3.0",
"multiformats": "^13.2.2",
"protons-runtime": "^5.5.0",
"timestamp-nano": "^1.0.1",
"uint8arraylist": "^2.4.8",
"uint8arrays": "^5.0.1"
"uint8arrays": "^5.1.0"
},
"devDependencies": {
"@libp2p/peer-id-factory": "^4.0.2",
"@libp2p/peer-id": "^5.0.0",
"aegir": "^44.1.1",
"protons": "^7.3.3"
"protons": "^7.6.0"
},
"sideEffects": false
}
84 changes: 71 additions & 13 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,71 @@
export const ERR_IPNS_EXPIRED_RECORD = 'ERR_IPNS_EXPIRED_RECORD'
export const ERR_UNRECOGNIZED_VALIDITY = 'ERR_UNRECOGNIZED_VALIDITY'
export const ERR_SIGNATURE_CREATION = 'ERR_SIGNATURE_CREATION'
export const ERR_SIGNATURE_VERIFICATION = 'ERR_SIGNATURE_VERIFICATION'
export const ERR_UNRECOGNIZED_FORMAT = 'ERR_UNRECOGNIZED_FORMAT'
export const ERR_PEER_ID_FROM_PUBLIC_KEY = 'ERR_PEER_ID_FROM_PUBLIC_KEY'
export const ERR_PUBLIC_KEY_FROM_ID = 'ERR_PUBLIC_KEY_FROM_ID'
export const ERR_UNDEFINED_PARAMETER = 'ERR_UNDEFINED_PARAMETER'
export const ERR_INVALID_RECORD_DATA = 'ERR_INVALID_RECORD_DATA'
export const ERR_INVALID_VALUE = 'ERR_INVALID_VALUE'
export const ERR_INVALID_EMBEDDED_KEY = 'ERR_INVALID_EMBEDDED_KEY'
export const ERR_MISSING_PRIVATE_KEY = 'ERR_MISSING_PRIVATE_KEY'
export const ERR_RECORD_TOO_LARGE = 'ERR_RECORD_TOO_LARGE'
export class SignatureCreationError extends Error {
static name = 'SignatureCreationError'

constructor (message = 'Record signature creation failed') {
super(message)
this.name = 'SignatureCreationError'
}

Check warning on line 7 in src/errors.ts

View check run for this annotation

Codecov / codecov/patch

src/errors.ts#L5-L7

Added lines #L5 - L7 were not covered by tests
}

export class SignatureVerificationError extends Error {
static name = 'SignatureVerificationError'

constructor (message = 'Record signature verification failed') {
super(message)
this.name = 'SignatureVerificationError'
}
}

export class RecordExpiredError extends Error {
static name = 'RecordExpiredError'

constructor (message = 'Record has expired') {
super(message)
this.name = 'RecordExpiredError'
}
}

export class UnsupportedValidityError extends Error {
static name = 'UnsupportedValidityError'

constructor (message = 'The validity type is unsupported') {
super(message)
this.name = 'UnsupportedValidityError'
}

Check warning on line 34 in src/errors.ts

View check run for this annotation

Codecov / codecov/patch

src/errors.ts#L32-L34

Added lines #L32 - L34 were not covered by tests
}

export class RecordTooLargeError extends Error {
static name = 'RecordTooLargeError'

constructor (message = 'The record is too large') {
super(message)
this.name = 'RecordTooLargeError'
}
}

export class InvalidValueError extends Error {
static name = 'InvalidValueError'

constructor (message = 'Value must be a valid content path starting with /') {
super(message)
this.name = 'InvalidValueError'
}
}

export class InvalidRecordDataError extends Error {
static name = 'InvalidRecordDataError'

constructor (message = 'Invalid record data') {
super(message)
this.name = 'InvalidRecordDataError'
}

Check warning on line 61 in src/errors.ts

View check run for this annotation

Codecov / codecov/patch

src/errors.ts#L59-L61

Added lines #L59 - L61 were not covered by tests
}

export class InvalidEmbeddedPublicKeyError extends Error {
static name = 'InvalidEmbeddedPublicKeyError'

constructor (message = 'Invalid embedded public key') {
super(message)
this.name = 'InvalidEmbeddedPublicKeyError'
}
}
84 changes: 28 additions & 56 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import { unmarshalPrivateKey } from '@libp2p/crypto/keys'
import { publicKeyToProtobuf } from '@libp2p/crypto/keys'
import { logger } from '@libp2p/logger'
import errCode from 'err-code'
import { Key } from 'interface-datastore/key'
import { base32upper } from 'multiformats/bases/base32'
import * as Digest from 'multiformats/hashes/digest'
import { identity } from 'multiformats/hashes/identity'
import { type Key } from 'interface-datastore/key'
import NanoDate from 'timestamp-nano'
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import * as ERRORS from './errors.js'
import { SignatureCreationError } from './errors.js'
import { IpnsEntry } from './pb/ipns.js'
import { createCborData, ipnsRecordDataForV1Sig, ipnsRecordDataForV2Sig, normalizeValue } from './utils.js'
import type { PrivateKey, PeerId } from '@libp2p/interface'
import type { PrivateKey, PublicKey } from '@libp2p/interface'
import type { CID } from 'multiformats/cid'
import type { MultihashDigest } from 'multiformats/hashes/interface'

const log = logger('ipns')
const ID_MULTIHASH_CODE = identity.code
const DEFAULT_TTL_NS = 60 * 60 * 1e+9 // 1 Hour or 3600 Seconds

export const namespace = '/ipns/'
Expand Down Expand Up @@ -157,22 +152,22 @@
* * PeerIDs will create recursive records, eg. the record value will be `/ipns/${cidV1Libp2pKey}`
* * String paths will be stored in the record as-is, but they must start with `"/"`
*
* @param {PeerId} peerId - peer id containing private key for signing the record.
* @param {CID | PeerId | string} value - content to be stored in the record.
* @param {PrivateKey} privateKey - the private key for signing the record.
* @param {CID | PublicKey | string} value - content to be stored in the record.
* @param {number | bigint} seq - number representing the current version of the record.
* @param {number} lifetime - lifetime of the record (in milliseconds).
* @param {CreateOptions} options - additional create options.
*/
export async function create (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, lifetime: number, options?: CreateV2OrV1Options): Promise<IPNSRecordV1V2>
export async function create (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, lifetime: number, options: CreateV2Options): Promise<IPNSRecordV2>
export async function create (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, lifetime: number, options: CreateOptions): Promise<IPNSRecordV1V2>
export async function create (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> {
export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, lifetime: number, options?: CreateV2OrV1Options): Promise<IPNSRecordV1V2>
export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, lifetime: number, options: CreateV2Options): Promise<IPNSRecordV2>
export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, lifetime: number, options: CreateOptions): Promise<IPNSRecordV1V2>
export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> {
// Validity in ISOString with nanoseconds precision and validity type EOL
const expirationDate = new NanoDate(Date.now() + Number(lifetime))
const validityType = IpnsEntry.ValidityType.EOL
const ttlNs = BigInt(options.ttlNs ?? DEFAULT_TTL_NS)

return _create(peerId, value, seq, validityType, expirationDate.toString(), ttlNs, options)
return _create(privateKey, value, seq, validityType, expirationDate.toString(), ttlNs, options)
}

/**
Expand All @@ -185,47 +180,37 @@
* * PeerIDs will create recursive records, eg. the record value will be `/ipns/${cidV1Libp2pKey}`
* * String paths will be stored in the record as-is, but they must start with `"/"`
*
* @param {PeerId} peerId - PeerId containing private key for signing the record.
* @param {CID | PeerId | string} value - content to be stored in the record.
* @param {PrivateKey} privateKey - the private key for signing the record.
* @param {CID | PublicKey | string} value - content to be stored in the record.
* @param {number | bigint} 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 {CreateOptions} options - additional creation options.
*/
export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options?: CreateV2OrV1Options): Promise<IPNSRecordV1V2>
export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options: CreateV2Options): Promise<IPNSRecordV2>
export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options: CreateOptions): Promise<IPNSRecordV1V2>
export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> {
export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, expiration: string, options?: CreateV2OrV1Options): Promise<IPNSRecordV1V2>
export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, expiration: string, options: CreateV2Options): Promise<IPNSRecordV2>
export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, expiration: string, options: CreateOptions): Promise<IPNSRecordV1V2>
export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> {
const expirationDate = NanoDate.fromString(expiration)
const validityType = IpnsEntry.ValidityType.EOL
const ttlNs = BigInt(options.ttlNs ?? DEFAULT_TTL_NS)

return _create(peerId, value, seq, validityType, expirationDate.toString(), ttlNs, options)
return _create(privateKey, value, seq, validityType, expirationDate.toString(), ttlNs, options)
}

const _create = async (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, validityType: IpnsEntry.ValidityType, validity: string, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
const _create = async (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, validityType: IpnsEntry.ValidityType, validity: string, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
seq = BigInt(seq)
const isoValidity = uint8ArrayFromString(validity)
const normalizedValue = normalizeValue(value)
const encodedValue = uint8ArrayFromString(normalizedValue)

if (peerId.privateKey == null) {
throw errCode(new Error('Missing private key'), ERRORS.ERR_MISSING_PRIVATE_KEY)
}

const privateKey = await unmarshalPrivateKey(peerId.privateKey)
const data = createCborData(encodedValue, validityType, isoValidity, seq, ttl)
const sigData = ipnsRecordDataForV2Sig(data)
const signatureV2 = await privateKey.sign(sigData)
let pubKey: Uint8Array | undefined

// if we cannot derive the public key from the PeerId (e.g. RSA PeerIDs),
// we have to embed it in the IPNS record
if (peerId.publicKey != null) {
const digest = Digest.decode(peerId.toBytes())

if (digest.code !== ID_MULTIHASH_CODE || !uint8ArrayEquals(peerId.publicKey, digest.digest)) {
pubKey = peerId.publicKey
}
if (privateKey.type === 'RSA') {
pubKey = publicKeyToProtobuf(privateKey.publicKey)
}

if (options.v1Compatible === true) {
Expand Down Expand Up @@ -266,24 +251,11 @@
}
}

/**
* rawStdEncoding with RFC4648
*/
const rawStdEncoding = (key: Uint8Array): string => base32upper.encode(key).slice(1)

/**
* Get key for storing the record locally.
* Format: /ipns/${base32(<HASH>)}
*
* @param {Uint8Array} key - peer identifier object.
*/
export const getLocalKey = (key: Uint8Array): Key => new Key(`/ipns/${rawStdEncoding(key)}`)

export { unmarshal } from './utils.js'
export { marshal } from './utils.js'
export { peerIdToRoutingKey } from './utils.js'
export { peerIdFromRoutingKey } from './utils.js'
export { extractPublicKey } from './utils.js'
export { unmarshalIPNSRecord } from './utils.js'
export { marshalIPNSRecord } from './utils.js'
export { multihashToIPNSRoutingKey } from './utils.js'
export { multihashFromIPNSRoutingKey } from './utils.js'
export { extractPublicKeyFromIPNSRecord } from './utils.js'

/**
* Sign ipns record data using the legacy V1 signature scheme
Expand All @@ -295,6 +267,6 @@
return await privateKey.sign(dataForSignature)
} catch (error: any) {
log.error('record signature creation failed', error)
throw errCode(new Error('record signature creation failed'), ERRORS.ERR_SIGNATURE_CREATION)
throw new SignatureCreationError('Record signature creation failed')

Check warning on line 270 in src/index.ts

View check run for this annotation

Codecov / codecov/patch

src/index.ts#L270

Added line #L270 was not covered by tests
}
}
39 changes: 24 additions & 15 deletions src/pb/ipns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */
/* eslint-disable @typescript-eslint/no-empty-interface */

import { enumeration, encodeMessage, decodeMessage, message } from 'protons-runtime'
import type { Codec } from 'protons-runtime'
import { type Codec, decodeMessage, type DecodeOptions, encodeMessage, enumeration, message } from 'protons-runtime'
import type { Uint8ArrayList } from 'uint8arraylist'

export interface IpnsEntry {
Expand Down Expand Up @@ -92,7 +91,7 @@
if (opts.lengthDelimited !== false) {
w.ldelim()
}
}, (reader, length) => {
}, (reader, length, opts = {}) => {
const obj: any = {}

const end = length == null ? reader.len : reader.pos + length
Expand All @@ -101,36 +100,46 @@
const tag = reader.uint32()

switch (tag >>> 3) {
case 1:
case 1: {
obj.value = reader.bytes()
break
case 2:
}
case 2: {
obj.signatureV1 = reader.bytes()
break
case 3:
}
case 3: {
obj.validityType = IpnsEntry.ValidityType.codec().decode(reader)
break
case 4:
}
case 4: {
obj.validity = reader.bytes()
break
case 5:
}
case 5: {
obj.sequence = reader.uint64()
break
case 6:
}
case 6: {
obj.ttl = reader.uint64()
break
case 7:
}
case 7: {
obj.pubKey = reader.bytes()
break
case 8:
}
case 8: {
obj.signatureV2 = reader.bytes()
break
case 9:
}
case 9: {
obj.data = reader.bytes()
break
default:
}
default: {
reader.skipType(tag & 7)
break
}

Check warning on line 142 in src/pb/ipns.ts

View check run for this annotation

Codecov / codecov/patch

src/pb/ipns.ts#L142

Added line #L142 was not covered by tests
}
}

Expand All @@ -145,7 +154,7 @@
return encodeMessage(obj, IpnsEntry.codec())
}

export const decode = (buf: Uint8Array | Uint8ArrayList): IpnsEntry => {
return decodeMessage(buf, IpnsEntry.codec())
export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions<IpnsEntry>): IpnsEntry => {
return decodeMessage(buf, IpnsEntry.codec(), opts)
}
}
Loading
Loading