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