1"use strict"; 2var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 if (k2 === undefined) k2 = k; 4 var desc = Object.getOwnPropertyDescriptor(m, k); 5 if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 desc = { enumerable: true, get: function() { return m[k]; } }; 7 } 8 Object.defineProperty(o, k2, desc); 9}) : (function(o, m, k, k2) { 10 if (k2 === undefined) k2 = k; 11 o[k2] = m[k]; 12})); 13var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 Object.defineProperty(o, "default", { enumerable: true, value: v }); 15}) : function(o, v) { 16 o["default"] = v; 17}); 18var __importStar = (this && this.__importStar) || function (mod) { 19 if (mod && mod.__esModule) return mod; 20 var result = {}; 21 if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 __setModuleDefault(result, mod); 23 return result; 24}; 25var __importDefault = (this && this.__importDefault) || function (mod) { 26 return (mod && mod.__esModule) ? mod : { "default": mod }; 27}; 28Object.defineProperty(exports, "__esModule", { value: true }); 29exports.Updater = void 0; 30const models_1 = require("@tufjs/models"); 31const debug_1 = __importDefault(require("debug")); 32const fs = __importStar(require("fs")); 33const path = __importStar(require("path")); 34const config_1 = require("./config"); 35const error_1 = require("./error"); 36const fetcher_1 = require("./fetcher"); 37const store_1 = require("./store"); 38const url = __importStar(require("./utils/url")); 39const log = (0, debug_1.default)('tuf:cache'); 40class Updater { 41 constructor(options) { 42 const { metadataDir, metadataBaseUrl, targetDir, targetBaseUrl, fetcher, config, } = options; 43 this.dir = metadataDir; 44 this.metadataBaseUrl = metadataBaseUrl; 45 this.targetDir = targetDir; 46 this.targetBaseUrl = targetBaseUrl; 47 this.forceCache = options.forceCache ?? false; 48 const data = this.loadLocalMetadata(models_1.MetadataKind.Root); 49 this.trustedSet = new store_1.TrustedMetadataStore(data); 50 this.config = { ...config_1.defaultConfig, ...config }; 51 this.fetcher = 52 fetcher || 53 new fetcher_1.DefaultFetcher({ 54 timeout: this.config.fetchTimeout, 55 retry: this.config.fetchRetries ?? this.config.fetchRetry, 56 }); 57 } 58 // refresh and load the metadata before downloading the target 59 // refresh should be called once after the client is initialized 60 async refresh() { 61 // If forceCache is true, try to load the timestamp from local storage 62 // without fetching it from the remote. Otherwise, load the root and 63 // timestamp from the remote per the TUF spec. 64 if (this.forceCache) { 65 // If anything fails, load the root and timestamp from the remote. This 66 // should cover any situation where the local metadata is corrupted or 67 // expired. 68 try { 69 await this.loadTimestamp({ checkRemote: false }); 70 } 71 catch (error) { 72 await this.loadRoot(); 73 await this.loadTimestamp(); 74 } 75 } 76 else { 77 await this.loadRoot(); 78 await this.loadTimestamp(); 79 } 80 await this.loadSnapshot(); 81 await this.loadTargets(models_1.MetadataKind.Targets, models_1.MetadataKind.Root); 82 } 83 // Returns the TargetFile instance with information for the given target path. 84 // 85 // Implicitly calls refresh if it hasn't already been called. 86 async getTargetInfo(targetPath) { 87 if (!this.trustedSet.targets) { 88 await this.refresh(); 89 } 90 return this.preorderDepthFirstWalk(targetPath); 91 } 92 async downloadTarget(targetInfo, filePath, targetBaseUrl) { 93 const targetPath = filePath || this.generateTargetPath(targetInfo); 94 if (!targetBaseUrl) { 95 if (!this.targetBaseUrl) { 96 throw new error_1.ValueError('Target base URL not set'); 97 } 98 targetBaseUrl = this.targetBaseUrl; 99 } 100 let targetFilePath = targetInfo.path; 101 const consistentSnapshot = this.trustedSet.root.signed.consistentSnapshot; 102 if (consistentSnapshot && this.config.prefixTargetsWithHash) { 103 const hashes = Object.values(targetInfo.hashes); 104 const { dir, base } = path.parse(targetFilePath); 105 const filename = `${hashes[0]}.${base}`; 106 targetFilePath = dir ? `${dir}/${filename}` : filename; 107 } 108 const targetUrl = url.join(targetBaseUrl, targetFilePath); 109 // Client workflow 5.7.3: download target file 110 await this.fetcher.downloadFile(targetUrl, targetInfo.length, async (fileName) => { 111 // Verify hashes and length of downloaded file 112 await targetInfo.verify(fs.createReadStream(fileName)); 113 // Copy file to target path 114 log('WRITE %s', targetPath); 115 fs.copyFileSync(fileName, targetPath); 116 }); 117 return targetPath; 118 } 119 async findCachedTarget(targetInfo, filePath) { 120 if (!filePath) { 121 filePath = this.generateTargetPath(targetInfo); 122 } 123 try { 124 if (fs.existsSync(filePath)) { 125 await targetInfo.verify(fs.createReadStream(filePath)); 126 return filePath; 127 } 128 } 129 catch (error) { 130 return; // File not found 131 } 132 return; // File not found 133 } 134 loadLocalMetadata(fileName) { 135 const filePath = path.join(this.dir, `${fileName}.json`); 136 log('READ %s', filePath); 137 return fs.readFileSync(filePath); 138 } 139 // Sequentially load and persist on local disk every newer root metadata 140 // version available on the remote. 141 // Client workflow 5.3: update root role 142 async loadRoot() { 143 // Client workflow 5.3.2: version of trusted root metadata file 144 const rootVersion = this.trustedSet.root.signed.version; 145 const lowerBound = rootVersion + 1; 146 const upperBound = lowerBound + this.config.maxRootRotations; 147 for (let version = lowerBound; version <= upperBound; version++) { 148 const rootUrl = url.join(this.metadataBaseUrl, `${version}.root.json`); 149 try { 150 // Client workflow 5.3.3: download new root metadata file 151 const bytesData = await this.fetcher.downloadBytes(rootUrl, this.config.rootMaxLength); 152 // Client workflow 5.3.4 - 5.4.7 153 this.trustedSet.updateRoot(bytesData); 154 // Client workflow 5.3.8: persist root metadata file 155 this.persistMetadata(models_1.MetadataKind.Root, bytesData); 156 } 157 catch (error) { 158 break; 159 } 160 } 161 } 162 // Load local and remote timestamp metadata. 163 // Client workflow 5.4: update timestamp role 164 async loadTimestamp({ checkRemote } = { checkRemote: true }) { 165 // Load local and remote timestamp metadata 166 try { 167 const data = this.loadLocalMetadata(models_1.MetadataKind.Timestamp); 168 this.trustedSet.updateTimestamp(data); 169 // If checkRemote is disabled, return here to avoid fetching the remote 170 // timestamp metadata. 171 if (!checkRemote) { 172 return; 173 } 174 } 175 catch (error) { 176 // continue 177 } 178 //Load from remote (whether local load succeeded or not) 179 const timestampUrl = url.join(this.metadataBaseUrl, 'timestamp.json'); 180 // Client workflow 5.4.1: download timestamp metadata file 181 const bytesData = await this.fetcher.downloadBytes(timestampUrl, this.config.timestampMaxLength); 182 try { 183 // Client workflow 5.4.2 - 5.4.4 184 this.trustedSet.updateTimestamp(bytesData); 185 } 186 catch (error) { 187 // If new timestamp version is same as current, discardd the new one. 188 // This is normal and should NOT raise an error. 189 if (error instanceof error_1.EqualVersionError) { 190 return; 191 } 192 // Re-raise any other error 193 throw error; 194 } 195 // Client workflow 5.4.5: persist timestamp metadata 196 this.persistMetadata(models_1.MetadataKind.Timestamp, bytesData); 197 } 198 // Load local and remote snapshot metadata. 199 // Client workflow 5.5: update snapshot role 200 async loadSnapshot() { 201 //Load local (and if needed remote) snapshot metadata 202 try { 203 const data = this.loadLocalMetadata(models_1.MetadataKind.Snapshot); 204 this.trustedSet.updateSnapshot(data, true); 205 } 206 catch (error) { 207 if (!this.trustedSet.timestamp) { 208 throw new ReferenceError('No timestamp metadata'); 209 } 210 const snapshotMeta = this.trustedSet.timestamp.signed.snapshotMeta; 211 const maxLength = snapshotMeta.length || this.config.snapshotMaxLength; 212 const version = this.trustedSet.root.signed.consistentSnapshot 213 ? snapshotMeta.version 214 : undefined; 215 const snapshotUrl = url.join(this.metadataBaseUrl, version ? `${version}.snapshot.json` : 'snapshot.json'); 216 try { 217 // Client workflow 5.5.1: download snapshot metadata file 218 const bytesData = await this.fetcher.downloadBytes(snapshotUrl, maxLength); 219 // Client workflow 5.5.2 - 5.5.6 220 this.trustedSet.updateSnapshot(bytesData); 221 // Client workflow 5.5.7: persist snapshot metadata file 222 this.persistMetadata(models_1.MetadataKind.Snapshot, bytesData); 223 } 224 catch (error) { 225 throw new error_1.RuntimeError(`Unable to load snapshot metadata error ${error}`); 226 } 227 } 228 } 229 // Load local and remote targets metadata. 230 // Client workflow 5.6: update targets role 231 async loadTargets(role, parentRole) { 232 if (this.trustedSet.getRole(role)) { 233 return this.trustedSet.getRole(role); 234 } 235 try { 236 const buffer = this.loadLocalMetadata(role); 237 this.trustedSet.updateDelegatedTargets(buffer, role, parentRole); 238 } 239 catch (error) { 240 // Local 'role' does not exist or is invalid: update from remote 241 if (!this.trustedSet.snapshot) { 242 throw new ReferenceError('No snapshot metadata'); 243 } 244 const metaInfo = this.trustedSet.snapshot.signed.meta[`${role}.json`]; 245 // TODO: use length for fetching 246 const maxLength = metaInfo.length || this.config.targetsMaxLength; 247 const version = this.trustedSet.root.signed.consistentSnapshot 248 ? metaInfo.version 249 : undefined; 250 const metadataUrl = url.join(this.metadataBaseUrl, version ? `${version}.${role}.json` : `${role}.json`); 251 try { 252 // Client workflow 5.6.1: download targets metadata file 253 const bytesData = await this.fetcher.downloadBytes(metadataUrl, maxLength); 254 // Client workflow 5.6.2 - 5.6.6 255 this.trustedSet.updateDelegatedTargets(bytesData, role, parentRole); 256 // Client workflow 5.6.7: persist targets metadata file 257 this.persistMetadata(role, bytesData); 258 } 259 catch (error) { 260 throw new error_1.RuntimeError(`Unable to load targets error ${error}`); 261 } 262 } 263 return this.trustedSet.getRole(role); 264 } 265 async preorderDepthFirstWalk(targetPath) { 266 // Interrogates the tree of target delegations in order of appearance 267 // (which implicitly order trustworthiness), and returns the matching 268 // target found in the most trusted role. 269 // List of delegations to be interrogated. A (role, parent role) pair 270 // is needed to load and verify the delegated targets metadata. 271 const delegationsToVisit = [ 272 { 273 roleName: models_1.MetadataKind.Targets, 274 parentRoleName: models_1.MetadataKind.Root, 275 }, 276 ]; 277 const visitedRoleNames = new Set(); 278 // Client workflow 5.6.7: preorder depth-first traversal of the graph of 279 // target delegations 280 while (visitedRoleNames.size <= this.config.maxDelegations && 281 delegationsToVisit.length > 0) { 282 // Pop the role name from the top of the stack. 283 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 284 const { roleName, parentRoleName } = delegationsToVisit.pop(); 285 // Skip any visited current role to prevent cycles. 286 // Client workflow 5.6.7.1: skip already-visited roles 287 if (visitedRoleNames.has(roleName)) { 288 continue; 289 } 290 // The metadata for 'role_name' must be downloaded/updated before 291 // its targets, delegations, and child roles can be inspected. 292 const targets = (await this.loadTargets(roleName, parentRoleName)) 293 ?.signed; 294 if (!targets) { 295 continue; 296 } 297 const target = targets.targets?.[targetPath]; 298 if (target) { 299 return target; 300 } 301 // After preorder check, add current role to set of visited roles. 302 visitedRoleNames.add(roleName); 303 if (targets.delegations) { 304 const childRolesToVisit = []; 305 // NOTE: This may be a slow operation if there are many delegated roles. 306 const rolesForTarget = targets.delegations.rolesForTarget(targetPath); 307 for (const { role: childName, terminating } of rolesForTarget) { 308 childRolesToVisit.push({ 309 roleName: childName, 310 parentRoleName: roleName, 311 }); 312 // Client workflow 5.6.7.2.1 313 if (terminating) { 314 delegationsToVisit.splice(0); // empty the array 315 break; 316 } 317 } 318 childRolesToVisit.reverse(); 319 delegationsToVisit.push(...childRolesToVisit); 320 } 321 } 322 return; // no matching target found 323 } 324 generateTargetPath(targetInfo) { 325 if (!this.targetDir) { 326 throw new error_1.ValueError('Target directory not set'); 327 } 328 // URL encode target path 329 const filePath = encodeURIComponent(targetInfo.path); 330 return path.join(this.targetDir, filePath); 331 } 332 persistMetadata(metaDataName, bytesData) { 333 try { 334 const filePath = path.join(this.dir, `${metaDataName}.json`); 335 log('WRITE %s', filePath); 336 fs.writeFileSync(filePath, bytesData.toString('utf8')); 337 } 338 catch (error) { 339 throw new error_1.PersistError(`Failed to persist metadata ${metaDataName} error: ${error}`); 340 } 341 } 342} 343exports.Updater = Updater; 344