@ndn/packet
Version:
NDNts: Network Layer Packets
268 lines (267 loc) • 10.1 kB
JavaScript
import { Encoder, EvDecoder, NNI } from "@ndn/tlv";
import { assert, constrain, sha256 } from "@ndn/util";
import { TT } from "./an_node.js";
import { FwHint } from "./fwhint_node.js";
import { definePublicFields, FIELDS } from "./impl-public-fields_node.js";
import { Name, ParamsDigest } from "./name/mod_node.js";
import { LLSign, LLVerify } from "./security/signing_node.js";
import { SigInfo } from "./sig-info_node.js";
const HOPLIMIT_MAX = 255;
class Fields {
constructor(...args) {
for (const arg of args) {
if (Name.isNameLike(arg)) {
this.name = Name.from(arg);
}
else if (arg instanceof FwHint) {
this.fwHint = new FwHint(arg);
}
else if (arg instanceof Uint8Array) {
this.appParameters = arg;
}
else if (arg instanceof Interest) {
Object.assign(this, arg[FIELDS]);
}
else if (arg[ctorAssign]) {
arg[ctorAssign](this);
}
else {
throw new Error("unknown Interest constructor argument");
}
}
}
name = new Name();
canBePrefix = false;
mustBeFresh = false;
fwHint;
get nonce() { return this.nonce_; }
set nonce(v) { this.nonce_ = v && constrain(v, "Nonce", 0xFFFFFFFF); }
nonce_;
get lifetime() { return this.lifetime_; }
set lifetime(v) { this.lifetime_ = constrain(Math.trunc(v), "InterestLifetime"); }
lifetime_ = Interest.DefaultLifetime;
get hopLimit() { return this.hopLimit_; }
set hopLimit(v) { this.hopLimit_ = constrain(v, "HopLimit", HOPLIMIT_MAX); }
hopLimit_ = HOPLIMIT_MAX;
appParameters;
sigInfo;
sigValue = new Uint8Array();
paramsPortion;
signedPortion;
}
const EVD = new EvDecoder("Interest", TT.Interest)
.add(TT.Name, (t, { decoder }) => t.name = decoder.decode(Name), { required: true })
.add(TT.CanBePrefix, (t) => t.canBePrefix = true)
.add(TT.MustBeFresh, (t) => t.mustBeFresh = true)
.add(TT.ForwardingHint, (t, { vd }) => t.fwHint = FwHint.decodeValue(vd))
.add(TT.Nonce, (t, { value }) => t.nonce = NNI.decode(value, { len: 4 }))
.add(TT.InterestLifetime, (t, { nni }) => t.lifetime = nni)
.add(TT.HopLimit, (t, { value }) => t.hopLimit = NNI.decode(value, { len: 1 }))
.add(TT.AppParameters, (t, { value, tlv, after }) => {
if (ParamsDigest.findIn(t.name, false) < 0) {
throw new Error("ParamsDigest missing in parameterized Interest");
}
t.appParameters = value;
assert(tlv.buffer === after.buffer);
t.paramsPortion = new Uint8Array(tlv.buffer, tlv.byteOffset, tlv.byteLength + after.byteLength);
})
.add(TT.ISigInfo, (t, { decoder }) => t.sigInfo = decoder.decode(SigInfo))
.add(TT.ISigValue, (t, { value, tlv }) => {
if (!t.name.get(-1)?.is(ParamsDigest)) {
throw new Error("ParamsDigest missing or out of place in signed Interest");
}
if (!t.paramsPortion) {
throw new Error("AppParameters missing in signed Interest");
}
if (!t.sigInfo) {
throw new Error("ISigInfo missing in signed Interest");
}
assert(tlv.buffer === t.paramsPortion.buffer);
t.sigValue = value;
// t.name.value should be readily available during decoding;
// t.name.getPrefix(-1).value would require re-encoding from components
const signedPart0 = t.name.value.subarray(0, -t.name.get(-1).tlv.byteLength);
const signedPart1 = new Uint8Array(tlv.buffer, t.paramsPortion.byteOffset, tlv.byteOffset - t.paramsPortion.byteOffset);
t.signedPortion = new Uint8Array(signedPart0.byteLength + signedPart1.byteLength);
t.signedPortion.set(signedPart0, 0);
t.signedPortion.set(signedPart1, signedPart0.byteLength);
});
/** Interest packet. */
export class Interest {
/**
* Construct from flexible arguments.
*
* Arguments can include, in any order:
* - {@link Interest} to copy from
* - {@link Name} or name URI
* - {@link Interest.CanBePrefix}
* - {@link Interest.MustBeFresh}
* - {@link FwHint}
* - {@link Interest.Nonce}`(v)`
* - {@link Interest.Lifetime}`(v)`
* - {@link Interest.HopLimit}`(v)`
* - `Uint8Array` as AppParameters
*/
constructor(...args) {
this[FIELDS] = new Fields(...args);
}
[FIELDS];
static decodeFrom(decoder) {
const interest = new Interest();
EVD.decode(interest[FIELDS], decoder);
return interest;
}
encodeTo(encoder) {
const { name, canBePrefix, mustBeFresh, fwHint, nonce, lifetime, hopLimit, appParameters } = this[FIELDS];
if (name.length === 0) {
throw new Error("invalid empty Interest name");
}
if (appParameters && ParamsDigest.findIn(name, false) < 0) {
throw new Error("ParamsDigest missing");
}
encoder.prependTlv(TT.Interest, name, canBePrefix && [TT.CanBePrefix], mustBeFresh && [TT.MustBeFresh], fwHint, [TT.Nonce, NNI(nonce ?? Interest.generateNonce(), { len: 4 })], lifetime !== Interest.DefaultLifetime && [TT.InterestLifetime, NNI(lifetime)], hopLimit !== HOPLIMIT_MAX && [TT.HopLimit, NNI(hopLimit, { len: 1 })], ...this.encodeParamsPortion());
}
encodeParamsPortion() {
const { appParameters, sigInfo, sigValue } = this[FIELDS];
if (!appParameters) {
return [];
}
const w = [[TT.AppParameters, appParameters]];
if (sigInfo) {
w.push(sigInfo.encodeAs(TT.ISigInfo), [TT.ISigValue, sigValue]);
}
return w;
}
appendParamsDigestPlaceholder() {
const f = this[FIELDS];
this.name = f.name.append(ParamsDigest.PLACEHOLDER);
return f.name.length - 1;
}
async updateParamsDigest() {
const f = this[FIELDS];
let pdIndex = ParamsDigest.findIn(f.name);
if (pdIndex < 0) {
pdIndex = this.appendParamsDigestPlaceholder();
}
f.appParameters ??= new Uint8Array();
f.paramsPortion = Encoder.encode(this.encodeParamsPortion());
const d = await sha256(f.paramsPortion);
f.name = f.name.replaceAt(pdIndex, ParamsDigest.create(d));
}
async validateParamsDigest(requireAppParameters = false) {
const { appParameters, paramsPortion, name } = this[FIELDS];
if (!appParameters) {
if (requireAppParameters) {
throw new Error("AppParameters is missing");
}
return;
}
if (!paramsPortion) {
throw new Error("parameters portion is empty");
}
const pdComp = name.at(ParamsDigest.findIn(name, false));
const d = await sha256(paramsPortion);
// This is not a constant-time comparison. It's for integrity purpose only.
if (!pdComp.equals(ParamsDigest.create(d))) {
throw new Error("incorrect ParamsDigest");
}
}
async [LLSign.OP](sign) {
const f = this[FIELDS];
let pdIndex = ParamsDigest.findIn(f.name);
if (pdIndex < 0) {
pdIndex = this.appendParamsDigestPlaceholder();
}
else if (pdIndex !== f.name.length - 1) {
throw new Error("ParamsDigest out of place for signed Interest");
}
f.signedPortion = Encoder.encode([
...f.name.getPrefix(-1).comps,
[TT.AppParameters, f.appParameters],
f.sigInfo?.encodeAs(TT.ISigInfo),
]);
this.sigValue = await sign(f.signedPortion);
return this.updateParamsDigest();
}
async [LLVerify.OP](verify) {
const { signedPortion, sigValue } = this[FIELDS];
await this.validateParamsDigest();
if (!signedPortion) {
throw new Error("SignedPortion is missing");
}
await verify(signedPortion, sigValue);
}
}
definePublicFields(Interest, {
name: ["signedPortion"],
canBePrefix: [],
mustBeFresh: [],
fwHint: [],
nonce: [],
lifetime: [],
hopLimit: [],
appParameters: ["paramsPortion", "signedPortion"],
sigInfo: ["paramsPortion", "signedPortion"],
sigValue: ["paramsPortion"],
});
const ctorAssign = Symbol("@ndn/packet#Interest.ctorAssign");
const modifyFields = [
"canBePrefix", "mustBeFresh", "fwHint", "lifetime", "hopLimit",
];
(function (Interest) {
/** Generate a random nonce. */
function generateNonce() {
return Math.trunc(Math.random() * 0x100000000);
}
Interest.generateNonce = generateNonce;
/** Default InterestLifetime. */
Interest.DefaultLifetime = 4000;
/** Constructor argument to set CanBePrefix flag. */
Interest.CanBePrefix = {
[ctorAssign](f) { f.canBePrefix = true; },
};
/** Constructor argument to set MustBeFresh flag. */
Interest.MustBeFresh = {
[ctorAssign](f) { f.mustBeFresh = true; },
};
/** Constructor argument to set Nonce field. */
function Nonce(v = generateNonce()) {
return {
[ctorAssign](f) { f.nonce = v; },
};
}
Interest.Nonce = Nonce;
/** Constructor argument to set InterestLifetime field. */
function Lifetime(v) {
return {
[ctorAssign](f) { f.lifetime = v; },
};
}
Interest.Lifetime = Lifetime;
/** Constructor argument to set HopLimit field. */
function HopLimit(v) {
return {
[ctorAssign](f) { f.hopLimit = v; },
};
}
Interest.HopLimit = HopLimit;
/**
* Turn {@link ModifyFields} to {@link ModifyFunc}.
* Return {@link ModifyFunc} as-is.
*/
function makeModifyFunc(input = () => undefined) {
if (typeof input === "function") {
return input;
}
const patch = {};
for (const key of modifyFields) {
if (input[key] !== undefined) {
patch[key] = input[key];
}
}
return (interest) => {
Object.assign(interest, patch);
};
}
Interest.makeModifyFunc = makeModifyFunc;
})(Interest || (Interest = {}));