ipns
Version:
IPNS record definitions
264 lines • 9.08 kB
JavaScript
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