Skip to content

Commit

Permalink
fix!: update to libp2p@2.x.x deps (#322)
Browse files Browse the repository at this point in the history
- Updates method names so they can be individually imported but still be legible
- Operates on PrivateKey/PublicKeys instead of PeerIds to integrate with the libp2p@2.x.x keychain will less friction

BREAKING CHANGE: uses libp2p@2.x.x deps, operates on PrivateKey/PublicKeys instead of PeerIds
  • Loading branch information
achingbrain committed Sep 12, 2024
1 parent 07e5ef9 commit 316910c
Show file tree
Hide file tree
Showing 13 changed files with 467 additions and 388 deletions.
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'
}
}

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'
}
}

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'
}
}

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 @@ const defaultCreateOptions: CreateOptions = {
* * 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 @@ export async function create (peerId: PeerId, value: CID | PeerId | string, seq:
* * 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 @@ const _create = async (peerId: PeerId, value: CID | PeerId | string, seq: number
}
}

/**
* 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 @@ const signLegacyV1 = async (privateKey: PrivateKey, value: Uint8Array, validityT
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')
}
}
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 @@ export namespace IpnsEntry {
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 @@ export namespace IpnsEntry {
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
}
}
}

Expand All @@ -145,7 +154,7 @@ export namespace IpnsEntry {
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

0 comments on commit 316910c

Please sign in to comment.