• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"use strict";
2var __importDefault = (this && this.__importDefault) || function (mod) {
3    return (mod && mod.__esModule) ? mod : { "default": mod };
4};
5Object.defineProperty(exports, "__esModule", { value: true });
6exports.SuccinctRoles = exports.DelegatedRole = exports.Role = exports.TOP_LEVEL_ROLE_NAMES = void 0;
7const crypto_1 = __importDefault(require("crypto"));
8const minimatch_1 = require("minimatch");
9const util_1 = __importDefault(require("util"));
10const error_1 = require("./error");
11const utils_1 = require("./utils");
12exports.TOP_LEVEL_ROLE_NAMES = [
13    'root',
14    'targets',
15    'snapshot',
16    'timestamp',
17];
18/**
19 * Container that defines which keys are required to sign roles metadata.
20 *
21 * Role defines how many keys are required to successfully sign the roles
22 * metadata, and which keys are accepted.
23 */
24class Role {
25    constructor(options) {
26        const { keyIDs, threshold, unrecognizedFields } = options;
27        if (hasDuplicates(keyIDs)) {
28            throw new error_1.ValueError('duplicate key IDs found');
29        }
30        if (threshold < 1) {
31            throw new error_1.ValueError('threshold must be at least 1');
32        }
33        this.keyIDs = keyIDs;
34        this.threshold = threshold;
35        this.unrecognizedFields = unrecognizedFields || {};
36    }
37    equals(other) {
38        if (!(other instanceof Role)) {
39            return false;
40        }
41        return (this.threshold === other.threshold &&
42            util_1.default.isDeepStrictEqual(this.keyIDs, other.keyIDs) &&
43            util_1.default.isDeepStrictEqual(this.unrecognizedFields, other.unrecognizedFields));
44    }
45    toJSON() {
46        return {
47            keyids: this.keyIDs,
48            threshold: this.threshold,
49            ...this.unrecognizedFields,
50        };
51    }
52    static fromJSON(data) {
53        const { keyids, threshold, ...rest } = data;
54        if (!utils_1.guard.isStringArray(keyids)) {
55            throw new TypeError('keyids must be an array');
56        }
57        if (typeof threshold !== 'number') {
58            throw new TypeError('threshold must be a number');
59        }
60        return new Role({
61            keyIDs: keyids,
62            threshold,
63            unrecognizedFields: rest,
64        });
65    }
66}
67exports.Role = Role;
68function hasDuplicates(array) {
69    return new Set(array).size !== array.length;
70}
71/**
72 * A container with information about a delegated role.
73 *
74 * A delegation can happen in two ways:
75 *   - ``paths`` is set: delegates targets matching any path pattern in ``paths``
76 *   - ``pathHashPrefixes`` is set: delegates targets whose target path hash
77 *      starts with any of the prefixes in ``pathHashPrefixes``
78 *
79 *   ``paths`` and ``pathHashPrefixes`` are mutually exclusive: both cannot be
80 *   set, at least one of them must be set.
81 */
82class DelegatedRole extends Role {
83    constructor(opts) {
84        super(opts);
85        const { name, terminating, paths, pathHashPrefixes } = opts;
86        this.name = name;
87        this.terminating = terminating;
88        if (opts.paths && opts.pathHashPrefixes) {
89            throw new error_1.ValueError('paths and pathHashPrefixes are mutually exclusive');
90        }
91        this.paths = paths;
92        this.pathHashPrefixes = pathHashPrefixes;
93    }
94    equals(other) {
95        if (!(other instanceof DelegatedRole)) {
96            return false;
97        }
98        return (super.equals(other) &&
99            this.name === other.name &&
100            this.terminating === other.terminating &&
101            util_1.default.isDeepStrictEqual(this.paths, other.paths) &&
102            util_1.default.isDeepStrictEqual(this.pathHashPrefixes, other.pathHashPrefixes));
103    }
104    isDelegatedPath(targetFilepath) {
105        if (this.paths) {
106            return this.paths.some((pathPattern) => isTargetInPathPattern(targetFilepath, pathPattern));
107        }
108        if (this.pathHashPrefixes) {
109            const hasher = crypto_1.default.createHash('sha256');
110            const pathHash = hasher.update(targetFilepath).digest('hex');
111            return this.pathHashPrefixes.some((pathHashPrefix) => pathHash.startsWith(pathHashPrefix));
112        }
113        return false;
114    }
115    toJSON() {
116        const json = {
117            ...super.toJSON(),
118            name: this.name,
119            terminating: this.terminating,
120        };
121        if (this.paths) {
122            json.paths = this.paths;
123        }
124        if (this.pathHashPrefixes) {
125            json.path_hash_prefixes = this.pathHashPrefixes;
126        }
127        return json;
128    }
129    static fromJSON(data) {
130        const { keyids, threshold, name, terminating, paths, path_hash_prefixes, ...rest } = data;
131        if (!utils_1.guard.isStringArray(keyids)) {
132            throw new TypeError('keyids must be an array of strings');
133        }
134        if (typeof threshold !== 'number') {
135            throw new TypeError('threshold must be a number');
136        }
137        if (typeof name !== 'string') {
138            throw new TypeError('name must be a string');
139        }
140        if (typeof terminating !== 'boolean') {
141            throw new TypeError('terminating must be a boolean');
142        }
143        if (utils_1.guard.isDefined(paths) && !utils_1.guard.isStringArray(paths)) {
144            throw new TypeError('paths must be an array of strings');
145        }
146        if (utils_1.guard.isDefined(path_hash_prefixes) &&
147            !utils_1.guard.isStringArray(path_hash_prefixes)) {
148            throw new TypeError('path_hash_prefixes must be an array of strings');
149        }
150        return new DelegatedRole({
151            keyIDs: keyids,
152            threshold,
153            name,
154            terminating,
155            paths,
156            pathHashPrefixes: path_hash_prefixes,
157            unrecognizedFields: rest,
158        });
159    }
160}
161exports.DelegatedRole = DelegatedRole;
162// JS version of Ruby's Array#zip
163const zip = (a, b) => a.map((k, i) => [k, b[i]]);
164function isTargetInPathPattern(target, pattern) {
165    const targetParts = target.split('/');
166    const patternParts = pattern.split('/');
167    if (patternParts.length != targetParts.length) {
168        return false;
169    }
170    return zip(targetParts, patternParts).every(([targetPart, patternPart]) => (0, minimatch_1.minimatch)(targetPart, patternPart));
171}
172/**
173 * Succinctly defines a hash bin delegation graph.
174 *
175 * A ``SuccinctRoles`` object describes a delegation graph that covers all
176 * targets, distributing them uniformly over the delegated roles (i.e. bins)
177 * in the graph.
178 *
179 * The total number of bins is 2 to the power of the passed ``bit_length``.
180 *
181 * Bin names are the concatenation of the passed ``name_prefix`` and a
182 * zero-padded hex representation of the bin index separated by a hyphen.
183 *
184 * The passed ``keyids`` and ``threshold`` is used for each bin, and each bin
185 * is 'terminating'.
186 *
187 * For details: https://github.com/theupdateframework/taps/blob/master/tap15.md
188 */
189class SuccinctRoles extends Role {
190    constructor(opts) {
191        super(opts);
192        const { bitLength, namePrefix } = opts;
193        if (bitLength <= 0 || bitLength > 32) {
194            throw new error_1.ValueError('bitLength must be between 1 and 32');
195        }
196        this.bitLength = bitLength;
197        this.namePrefix = namePrefix;
198        // Calculate the suffix_len value based on the total number of bins in
199        // hex. If bit_length = 10 then number_of_bins = 1024 or bin names will
200        // have a suffix between "000" and "3ff" in hex and suffix_len will be 3
201        // meaning the third bin will have a suffix of "003".
202        this.numberOfBins = Math.pow(2, bitLength);
203        // suffix_len is calculated based on "number_of_bins - 1" as the name
204        // of the last bin contains the number "number_of_bins -1" as a suffix.
205        this.suffixLen = (this.numberOfBins - 1).toString(16).length;
206    }
207    equals(other) {
208        if (!(other instanceof SuccinctRoles)) {
209            return false;
210        }
211        return (super.equals(other) &&
212            this.bitLength === other.bitLength &&
213            this.namePrefix === other.namePrefix);
214    }
215    /***
216     * Calculates the name of the delegated role responsible for 'target_filepath'.
217     *
218     * The target at path ''target_filepath' is assigned to a bin by casting
219     * the left-most 'bit_length' of bits of the file path hash digest to
220     * int, using it as bin index between 0 and '2**bit_length - 1'.
221     *
222     * Args:
223     *  target_filepath: URL path to a target file, relative to a base
224     *  targets URL.
225     */
226    getRoleForTarget(targetFilepath) {
227        const hasher = crypto_1.default.createHash('sha256');
228        const hasherBuffer = hasher.update(targetFilepath).digest();
229        // can't ever need more than 4 bytes (32 bits).
230        const hashBytes = hasherBuffer.subarray(0, 4);
231        // Right shift hash bytes, so that we only have the leftmost
232        // bit_length bits that we care about.
233        const shiftValue = 32 - this.bitLength;
234        const binNumber = hashBytes.readUInt32BE() >>> shiftValue;
235        // Add zero padding if necessary and cast to hex the suffix.
236        const suffix = binNumber.toString(16).padStart(this.suffixLen, '0');
237        return `${this.namePrefix}-${suffix}`;
238    }
239    *getRoles() {
240        for (let i = 0; i < this.numberOfBins; i++) {
241            const suffix = i.toString(16).padStart(this.suffixLen, '0');
242            yield `${this.namePrefix}-${suffix}`;
243        }
244    }
245    /***
246     * Determines whether the given ``role_name`` is in one of
247     * the delegated roles that ``SuccinctRoles`` represents.
248     *
249     * Args:
250     *  role_name: The name of the role to check against.
251     */
252    isDelegatedRole(roleName) {
253        const desiredPrefix = this.namePrefix + '-';
254        if (!roleName.startsWith(desiredPrefix)) {
255            return false;
256        }
257        const suffix = roleName.slice(desiredPrefix.length, roleName.length);
258        if (suffix.length != this.suffixLen) {
259            return false;
260        }
261        // make sure the suffix is a hex string
262        if (!suffix.match(/^[0-9a-fA-F]+$/)) {
263            return false;
264        }
265        const num = parseInt(suffix, 16);
266        return 0 <= num && num < this.numberOfBins;
267    }
268    toJSON() {
269        const json = {
270            ...super.toJSON(),
271            bit_length: this.bitLength,
272            name_prefix: this.namePrefix,
273        };
274        return json;
275    }
276    static fromJSON(data) {
277        const { keyids, threshold, bit_length, name_prefix, ...rest } = data;
278        if (!utils_1.guard.isStringArray(keyids)) {
279            throw new TypeError('keyids must be an array of strings');
280        }
281        if (typeof threshold !== 'number') {
282            throw new TypeError('threshold must be a number');
283        }
284        if (typeof bit_length !== 'number') {
285            throw new TypeError('bit_length must be a number');
286        }
287        if (typeof name_prefix !== 'string') {
288            throw new TypeError('name_prefix must be a string');
289        }
290        return new SuccinctRoles({
291            keyIDs: keyids,
292            threshold,
293            bitLength: bit_length,
294            namePrefix: name_prefix,
295            unrecognizedFields: rest,
296        });
297    }
298}
299exports.SuccinctRoles = SuccinctRoles;
300