@libp2p/peer-record
Version:
Used to transfer signed peer data across the network
149 lines (128 loc) • 4.69 kB
text/typescript
import { publicKeyFromProtobuf, publicKeyToProtobuf } from '@libp2p/crypto/keys'
import * as varint from 'uint8-varint'
import { Uint8ArrayList } from 'uint8arraylist'
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
import { fromString as uint8arraysFromString } from 'uint8arrays/from-string'
import { Envelope as Protobuf } from './envelope.js'
import { InvalidSignatureError } from './errors.js'
import type { Record, Envelope, PrivateKey, PublicKey } from '@libp2p/interface'
export interface RecordEnvelopeInit {
publicKey: PublicKey
payloadType: Uint8Array
payload: Uint8Array
signature: Uint8Array
}
export class RecordEnvelope implements Envelope {
/**
* Unmarshal a serialized Envelope protobuf message
*/
static createFromProtobuf = async (data: Uint8Array | Uint8ArrayList): Promise<RecordEnvelope> => {
const envelopeData = Protobuf.decode(data)
const publicKey = publicKeyFromProtobuf(envelopeData.publicKey)
return new RecordEnvelope({
publicKey,
payloadType: envelopeData.payloadType,
payload: envelopeData.payload,
signature: envelopeData.signature
})
}
/**
* Seal marshals the given Record, places the marshaled bytes inside an Envelope
* and signs it with the given peerId's private key
*/
static seal = async (record: Record, privateKey: PrivateKey): Promise<RecordEnvelope> => {
if (privateKey == null) {
throw new Error('Missing private key')
}
const domain = record.domain
const payloadType = record.codec
const payload = record.marshal()
const signData = formatSignaturePayload(domain, payloadType, payload)
const signature = await privateKey.sign(signData.subarray())
return new RecordEnvelope({
publicKey: privateKey.publicKey,
payloadType,
payload,
signature
})
}
/**
* Open and certify a given marshaled envelope.
* Data is unmarshaled and the signature validated for the given domain.
*/
static openAndCertify = async (data: Uint8Array | Uint8ArrayList, domain: string): Promise<RecordEnvelope> => {
const envelope = await RecordEnvelope.createFromProtobuf(data)
const valid = await envelope.validate(domain)
if (!valid) {
throw new InvalidSignatureError('Envelope signature is not valid for the given domain')
}
return envelope
}
public publicKey: PublicKey
public payloadType: Uint8Array
public payload: Uint8Array
public signature: Uint8Array
public marshaled?: Uint8Array
/**
* The Envelope is responsible for keeping an arbitrary signed record
* by a libp2p peer.
*/
constructor (init: RecordEnvelopeInit) {
const { publicKey, payloadType, payload, signature } = init
this.publicKey = publicKey
this.payloadType = payloadType
this.payload = payload
this.signature = signature
}
/**
* Marshal the envelope content
*/
marshal (): Uint8Array {
if (this.marshaled == null) {
this.marshaled = Protobuf.encode({
publicKey: publicKeyToProtobuf(this.publicKey),
payloadType: this.payloadType,
payload: this.payload.subarray(),
signature: this.signature
})
}
return this.marshaled
}
/**
* Verifies if the other Envelope is identical to this one
*/
equals (other: Envelope): boolean {
return uint8ArrayEquals(this.marshal(), other.marshal())
}
/**
* Validate envelope data signature for the given domain
*/
async validate (domain: string): Promise<boolean> {
const signData = formatSignaturePayload(domain, this.payloadType, this.payload)
return this.publicKey.verify(signData.subarray(), this.signature)
}
}
/**
* Helper function that prepares a Uint8Array to sign or verify a signature
*/
const formatSignaturePayload = (domain: string, payloadType: Uint8Array, payload: Uint8Array | Uint8ArrayList): Uint8ArrayList => {
// When signing, a peer will prepare a Uint8Array by concatenating the following:
// - The length of the domain separation string string in bytes
// - The domain separation string, encoded as UTF-8
// - The length of the payload_type field in bytes
// - The value of the payload_type field
// - The length of the payload field in bytes
// - The value of the payload field
const domainUint8Array = uint8arraysFromString(domain)
const domainLength = varint.encode(domainUint8Array.byteLength)
const payloadTypeLength = varint.encode(payloadType.length)
const payloadLength = varint.encode(payload.length)
return new Uint8ArrayList(
domainLength,
domainUint8Array,
payloadTypeLength,
payloadType,
payloadLength,
payload
)
}