UNPKG

ipns

Version:
264 lines 9.08 kB
import { publicKeyFromProtobuf } from '@libp2p/crypto/keys'; import { InvalidMultihashError } from '@libp2p/interface'; import { logger } from '@libp2p/logger'; import * as cborg from 'cborg'; import { base36 } from 'multiformats/bases/base36'; import { CID } from 'multiformats/cid'; import * as Digest from 'multiformats/hashes/digest'; 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 { InvalidRecordDataError, InvalidValueError, SignatureVerificationError, UnsupportedValidityError } from './errors.js'; import { IpnsEntry } from './pb/ipns.js'; const log = logger('ipns:utils'); const IPNS_PREFIX = uint8ArrayFromString('/ipns/'); const LIBP2P_CID_CODEC = 0x72; const IDENTITY_CODEC = 0x0; const SHA2_256_CODEC = 0x12; /** * Extracts a public key from the passed PeerId, falling back to the pubKey * embedded in the ipns record */ export function extractPublicKeyFromIPNSRecord(record) { let pubKey; if (record.pubKey != null) { try { pubKey = publicKeyFromProtobuf(record.pubKey); } catch (err) { log.error(err); throw err; } } if (pubKey != null) { return pubKey; } } /** * Utility for creating the record data for being signed */ export function ipnsRecordDataForV1Sig(value, validityType, validity) { const validityTypeBuffer = uint8ArrayFromString(validityType); return uint8ArrayConcat([value, validity, validityTypeBuffer]); } /** * Utility for creating the record data for being signed */ export function ipnsRecordDataForV2Sig(data) { const entryData = uint8ArrayFromString('ipns-signature:'); return uint8ArrayConcat([entryData, data]); } export function marshalIPNSRecord(obj) { if ('signatureV1' in obj) { return IpnsEntry.encode({ value: uint8ArrayFromString(obj.value), signatureV1: obj.signatureV1, validityType: obj.validityType, validity: uint8ArrayFromString(obj.validity), 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 unmarshalIPNSRecord(buf) { 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 new SignatureVerificationError('Missing data or signatureV2'); } const data = parseCborData(message.data); const value = normalizeByteValue(data.Value); const validity = uint8ArrayToString(data.Validity); 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 function multihashToIPNSRoutingKey(digest) { return uint8ArrayConcat([ IPNS_PREFIX, digest.bytes ]); } export function multihashFromIPNSRoutingKey(key) { const digest = Digest.decode(key.slice(IPNS_PREFIX.length)); if (!isCodec(digest, IDENTITY_CODEC) && !isCodec(digest, SHA2_256_CODEC)) { throw new InvalidMultihashError('Multihash in IPNS key was not identity or sha2-256'); } return digest; } export function createCborData(value, validityType, validity, sequence, ttl) { let ValidityType; if (validityType === IpnsEntry.ValidityType.EOL) { ValidityType = 0; } else { throw new UnsupportedValidityError('The validity type is unsupported'); } const data = { Value: value, Validity: validity, ValidityType, Sequence: sequence, TTL: ttl }; return cborg.encode(data); } export function parseCborData(buf) { const data = cborg.decode(buf); if (data.ValidityType === 0) { data.ValidityType = IpnsEntry.ValidityType.EOL; } else { throw new UnsupportedValidityError('The validity type is unsupported'); } 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; } export function normalizeByteValue(value) { const string = uint8ArrayToString(value).trim(); // if we have a path, check it is a valid path if (string.startsWith('/')) { return string; } // try parsing what we have as CID bytes or a CID string try { return `/ipfs/${CID.decode(value).toV1().toString()}`; } catch { // fall through } try { return `/ipfs/${CID.parse(string).toV1().toString()}`; } catch { // fall through } throw new InvalidValueError('Value must be a valid content path starting with /'); } /** * 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 function normalizeValue(value) { if (value != null) { const cid = asCID(value); // if we have a CID, turn it into an ipfs path if (cid != null) { // PeerID encoded as a CID if (cid.code === LIBP2P_CID_CODEC) { return `/ipns/${cid.toString(base36)}`; } return `/ipfs/${cid.toV1().toString()}`; } if (hasBytes(value)) { return `/ipns/${base36.encode(value.bytes)}`; } // if we have a path, check it is a valid path const string = value.toString().trim(); if (string.startsWith('/') && string.length > 1) { return string; } } throw new InvalidValueError('Value must be a valid content path starting with /'); } function validateCborDataMatchesPbData(entry) { if (entry.data == null) { throw new InvalidRecordDataError('Record data is missing'); } const data = parseCborData(entry.data); if (!uint8ArrayEquals(data.Value, entry.value ?? new Uint8Array(0))) { throw new SignatureVerificationError('Field "value" did not match between protobuf and CBOR'); } if (!uint8ArrayEquals(data.Validity, entry.validity ?? new Uint8Array(0))) { throw new SignatureVerificationError('Field "validity" did not match between protobuf and CBOR'); } if (data.ValidityType !== entry.validityType) { throw new SignatureVerificationError('Field "validityType" did not match between protobuf and CBOR'); } if (data.Sequence !== entry.sequence) { throw new SignatureVerificationError('Field "sequence" did not match between protobuf and CBOR'); } if (data.TTL !== entry.ttl) { throw new SignatureVerificationError('Field "ttl" did not match between protobuf and CBOR'); } } function hasBytes(obj) { return obj.bytes instanceof Uint8Array; } function hasToCID(obj) { return typeof obj?.toCID === 'function'; } function asCID(obj) { if (hasToCID(obj)) { return obj.toCID(); } // try parsing as a CID string try { return CID.parse(obj); } catch { // fall through } return CID.asCID(obj); } export function isCodec(digest, codec) { return digest.code === codec; } //# sourceMappingURL=utils.js.map