@slicedearth/whoisleuth-cli 1.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/LICENSE +661 -0
  2. package/LICENSES/Retire.js-Apache-2.0.txt +199 -0
  3. package/NOTICE +76 -0
  4. package/README.md +84 -0
  5. package/TRADEMARKS.md +41 -0
  6. package/bin/whoisleuth.mjs +35 -0
  7. package/cli/archive-inspect.mjs +198 -0
  8. package/cli/arguments.mjs +1022 -0
  9. package/cli/artifact-verify.mjs +210 -0
  10. package/cli/bulk-checkpoint.mjs +218 -0
  11. package/cli/bulk-command-runner.mjs +109 -0
  12. package/cli/bulk-output.mjs +112 -0
  13. package/cli/bulk.mjs +133 -0
  14. package/cli/compare.mjs +238 -0
  15. package/cli/completion.mjs +175 -0
  16. package/cli/csv.mjs +1 -0
  17. package/cli/discover.mjs +50 -0
  18. package/cli/discovery-command-runner.mjs +36 -0
  19. package/cli/discovery-observation-snapshot.mjs +315 -0
  20. package/cli/discovery-scan-command-runner.mjs +141 -0
  21. package/cli/discovery-scan.mjs +325 -0
  22. package/cli/discovery-snapshot.mjs +133 -0
  23. package/cli/discovery-workflow.mjs +73 -0
  24. package/cli/doctor.mjs +137 -0
  25. package/cli/errors.mjs +21 -0
  26. package/cli/evidence-command-arguments.mjs +145 -0
  27. package/cli/evidence-command-runner.mjs +106 -0
  28. package/cli/evidence-signing.mjs +215 -0
  29. package/cli/exit-codes.mjs +9 -0
  30. package/cli/export-evidence.mjs +28 -0
  31. package/cli/formatters/evidence-report.mjs +3 -0
  32. package/cli/formatters/html.mjs +69 -0
  33. package/cli/formatters/json.mjs +135 -0
  34. package/cli/formatters/markdown.mjs +3 -0
  35. package/cli/formatters/terminal.mjs +658 -0
  36. package/cli/http.mjs +27 -0
  37. package/cli/lookup-command-runner.mjs +87 -0
  38. package/cli/lookup-diff.mjs +171 -0
  39. package/cli/mail-review.mjs +204 -0
  40. package/cli/manual.mjs +34 -0
  41. package/cli/network-command-runner.mjs +88 -0
  42. package/cli/output-file.mjs +97 -0
  43. package/cli/page-compare.mjs +123 -0
  44. package/cli/posture.mjs +20 -0
  45. package/cli/progress-events.mjs +53 -0
  46. package/cli/progress.mjs +56 -0
  47. package/cli/registry-access.mjs +3 -0
  48. package/cli/registry-support.mjs +120 -0
  49. package/cli/risk-calibration.mjs +294 -0
  50. package/cli/runner-types.mjs +1 -0
  51. package/cli/runner.mjs +726 -0
  52. package/cli/saved-lookup.mjs +87 -0
  53. package/cli/source-reliability.mjs +727 -0
  54. package/cli/strict-exit.mjs +55 -0
  55. package/cli/terminal-presentation.mjs +147 -0
  56. package/docs/cli.md +827 -0
  57. package/frontend/src/lib/analysis/acquisition-decision-packet.js +93 -0
  58. package/frontend/src/lib/analysis/acquisition-due-diligence.js +391 -0
  59. package/frontend/src/lib/analysis/artifact-integrity.js +21 -0
  60. package/frontend/src/lib/analysis/brand-profile-model.js +421 -0
  61. package/frontend/src/lib/analysis/bulk-domain-comparison.js +246 -0
  62. package/frontend/src/lib/analysis/bulk-mail-exposure.js +192 -0
  63. package/frontend/src/lib/analysis/bulk-retry-plan.js +86 -0
  64. package/frontend/src/lib/analysis/bulk-review-export.js +62 -0
  65. package/frontend/src/lib/analysis/bulk-review-model.js +266 -0
  66. package/frontend/src/lib/analysis/bulk-session-model.js +515 -0
  67. package/frontend/src/lib/analysis/bulk-sort.js +49 -0
  68. package/frontend/src/lib/analysis/campaign-model.js +265 -0
  69. package/frontend/src/lib/analysis/case-evidence-model.js +642 -0
  70. package/frontend/src/lib/analysis/case-migration-model.js +248 -0
  71. package/frontend/src/lib/analysis/case-model.js +5 -0
  72. package/frontend/src/lib/analysis/case-record-contracts.js +44 -0
  73. package/frontend/src/lib/analysis/case-record-core.js +232 -0
  74. package/frontend/src/lib/analysis/case-record-model.js +7 -0
  75. package/frontend/src/lib/analysis/case-record-operations.js +224 -0
  76. package/frontend/src/lib/analysis/case-response-model.js +640 -0
  77. package/frontend/src/lib/analysis/case-response-packet.js +573 -0
  78. package/frontend/src/lib/analysis/case-storage-model.js +93 -0
  79. package/frontend/src/lib/analysis/detection-rule-model.js +306 -0
  80. package/frontend/src/lib/analysis/dns-record-normalization.js +11 -0
  81. package/frontend/src/lib/analysis/http-summary.js +165 -0
  82. package/frontend/src/lib/analysis/investigation-guide.js +500 -0
  83. package/frontend/src/lib/analysis/investigation-template-model.js +144 -0
  84. package/frontend/src/lib/analysis/page-baseline.js +272 -0
  85. package/frontend/src/lib/analysis/page-similarity.js +179 -0
  86. package/frontend/src/lib/analysis/relationship-evidence.js +265 -0
  87. package/frontend/src/lib/analysis/relationship-observation-model.js +423 -0
  88. package/frontend/src/lib/analysis/scoring.js +135 -0
  89. package/frontend/src/lib/analysis/shortlist-model.js +209 -0
  90. package/frontend/src/lib/analysis/utils.js +265 -0
  91. package/frontend/src/lib/analysis/watchlist-history.js +643 -0
  92. package/frontend/src/lib/analysis/watchlist-store.js +127 -0
  93. package/frontend/src/lib/analysis/website-snapshot-model.js +302 -0
  94. package/frontend/src/lib/analysis/workspace-archive-crypto.js +282 -0
  95. package/frontend/src/lib/analysis/workspace-archive.js +472 -0
  96. package/lib/abort.mjs +26 -0
  97. package/lib/abusech-auth.mjs +19 -0
  98. package/lib/availability.mjs +716 -0
  99. package/lib/bounded-contract-normalizers.mjs +61 -0
  100. package/lib/browser-library-profile.mjs +310 -0
  101. package/lib/bulk-comparison-evidence.mjs +83 -0
  102. package/lib/classify.mjs +93 -0
  103. package/lib/client-behavior-profile.mjs +83 -0
  104. package/lib/credential-surface-profile.mjs +66 -0
  105. package/lib/csv.mjs +23 -0
  106. package/lib/ct-query.mjs +27 -0
  107. package/lib/ct-search.mjs +393 -0
  108. package/lib/distributed-operation-budget.mjs +461 -0
  109. package/lib/dns-delegation-health.mjs +344 -0
  110. package/lib/dns-intelligence.mjs +739 -0
  111. package/lib/dns-mx.mjs +24 -0
  112. package/lib/dns-resolver-selection.mjs +33 -0
  113. package/lib/domain-posture-analysis.mjs +254 -0
  114. package/lib/domain-posture-parsers.mjs +358 -0
  115. package/lib/domain-posture.mjs +745 -0
  116. package/lib/error-detail.mjs +10 -0
  117. package/lib/evidence-export.mjs +431 -0
  118. package/lib/evidence-report-markdown.mjs +71 -0
  119. package/lib/evidence-report.mjs +391 -0
  120. package/lib/external-intelligence-risk.mjs +108 -0
  121. package/lib/favicon.mjs +146 -0
  122. package/lib/feature-policy.mjs +70 -0
  123. package/lib/generated/retire-browser-catalog.mjs +8724 -0
  124. package/lib/generated/unicode-confusables-17.mjs +93 -0
  125. package/lib/hostname.mjs +38 -0
  126. package/lib/html-signals.mjs +680 -0
  127. package/lib/http-intelligence.mjs +232 -0
  128. package/lib/idn-confusable-policy.mjs +86 -0
  129. package/lib/idn-confusables.mjs +465 -0
  130. package/lib/lookup-cache.mjs +130 -0
  131. package/lib/lookup-progress.mjs +291 -0
  132. package/lib/lookup-source-progress.mjs +109 -0
  133. package/lib/lookup-threat-provider-inventory.mjs +13 -0
  134. package/lib/lookup.mjs +592 -0
  135. package/lib/observation.mjs +114 -0
  136. package/lib/observed-network-context.mjs +355 -0
  137. package/lib/operation-budget.mjs +489 -0
  138. package/lib/page-fingerprints.mjs +481 -0
  139. package/lib/page-role-profile.mjs +117 -0
  140. package/lib/perceptual-hash.mjs +451 -0
  141. package/lib/rdap-attempts.mjs +34 -0
  142. package/lib/rdap-bootstrap.mjs +173 -0
  143. package/lib/rdap-capabilities.mjs +292 -0
  144. package/lib/rdap-client.mjs +125 -0
  145. package/lib/rdap-normalization.mjs +691 -0
  146. package/lib/rdap-registrar.mjs +196 -0
  147. package/lib/rdap-transport.mjs +39 -0
  148. package/lib/rdap-types.mjs +1 -0
  149. package/lib/rdap-validation.mjs +94 -0
  150. package/lib/rdap.mjs +16 -0
  151. package/lib/registry-access.mjs +32 -0
  152. package/lib/registry-capabilities.mjs +117 -0
  153. package/lib/registry-capability-catalogue.mjs +1206 -0
  154. package/lib/registry-comparison.mjs +318 -0
  155. package/lib/registry-dates.mjs +190 -0
  156. package/lib/registry-insights.mjs +325 -0
  157. package/lib/response-policy.mjs +256 -0
  158. package/lib/risk-scoring.mjs +164 -0
  159. package/lib/safe-fetch.mjs +446 -0
  160. package/lib/security-txt.mjs +359 -0
  161. package/lib/service-binding-dns.mjs +608 -0
  162. package/lib/sslbl-certificates.generated.mjs +143 -0
  163. package/lib/sslbl-intelligence.mjs +241 -0
  164. package/lib/static-html-analysis.mjs +340 -0
  165. package/lib/structured-data-identity.mjs +323 -0
  166. package/lib/threat-intelligence-contract.mjs +5 -0
  167. package/lib/threat-intelligence-definition-registry.mjs +34 -0
  168. package/lib/threat-intelligence-fixtures.mjs +82 -0
  169. package/lib/threat-intelligence-manifests.mjs +512 -0
  170. package/lib/threat-intelligence-runtime.mjs +662 -0
  171. package/lib/threat-intelligence-targets.mjs +168 -0
  172. package/lib/threat-intelligence-types.mjs +3 -0
  173. package/lib/threatfox-intelligence.mjs +307 -0
  174. package/lib/tls-intelligence.mjs +814 -0
  175. package/lib/typosquat-generator.mjs +884 -0
  176. package/lib/urlhaus-intelligence.mjs +361 -0
  177. package/lib/urlscan-intelligence.mjs +349 -0
  178. package/lib/website-security-posture.mjs +325 -0
  179. package/lib/website-technology.mjs +437 -0
  180. package/lib/whois-authority.mjs +98 -0
  181. package/lib/whois-chain.mjs +261 -0
  182. package/lib/whois-contacts.mjs +163 -0
  183. package/lib/whois-contracts.mjs +3 -0
  184. package/lib/whois-normalization.mjs +27 -0
  185. package/lib/whois-transport.mjs +100 -0
  186. package/lib/whois-values.mjs +92 -0
  187. package/lib/whois.mjs +1187 -0
  188. package/package.json +55 -0
  189. package/third-party-notices.txt +5571 -0
@@ -0,0 +1,282 @@
1
+ // Browser-local authenticated encryption for portable workspace archives.
2
+ // This protects a downloaded file while it is locked. It does not encrypt the
3
+ // active IndexedDB workspace or defend an unlocked page from same-origin code.
4
+ import { MAX_WORKSPACE_ARCHIVE_BYTES, WORKSPACE_ARCHIVE_SCHEMA, WORKSPACE_ARCHIVE_VERSION, isSupportedWorkspaceArchiveVersion, readWorkspaceArchive, } from "./workspace-archive.js";
5
+ export const ENCRYPTED_WORKSPACE_ARCHIVE_SCHEMA = 'whoisleuth.encrypted-workspace-archive';
6
+ export const ENCRYPTED_WORKSPACE_ARCHIVE_VERSION = 1;
7
+ export const WORKSPACE_ARCHIVE_PBKDF2_ITERATIONS = 600_000;
8
+ export const MIN_WORKSPACE_ARCHIVE_PASSPHRASE_CHARACTERS = 12;
9
+ export const MAX_WORKSPACE_ARCHIVE_PASSPHRASE_BYTES = 1024;
10
+ export const MAX_ENCRYPTED_WORKSPACE_ARCHIVE_BYTES = Math.ceil(MAX_WORKSPACE_ARCHIVE_BYTES * 4 / 3) + 4096;
11
+ const SALT_BYTES = 16;
12
+ const IV_BYTES = 12;
13
+ const AES_KEY_BITS = 256;
14
+ const AES_GCM_TAG_BITS = 128;
15
+ const MAX_CIPHERTEXT_BASE64URL_CHARACTERS = Math.ceil((MAX_WORKSPACE_ARCHIVE_BYTES + AES_GCM_TAG_BITS / 8) * 4 / 3);
16
+ const BASE64URL_RE = /^[A-Za-z0-9_-]+$/;
17
+ const ISO_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
18
+ const encoder = new TextEncoder();
19
+ const decoder = new TextDecoder('utf-8', { fatal: true });
20
+ function record(value) {
21
+ return value && typeof value === 'object' && !Array.isArray(value)
22
+ ? value
23
+ : null;
24
+ }
25
+ function hasExactKeys(value, expected) {
26
+ const keys = Object.keys(value);
27
+ return keys.length === expected.length && keys.every((key) => expected.includes(key));
28
+ }
29
+ function byteLength(value) {
30
+ return encoder.encode(value).byteLength;
31
+ }
32
+ function arrayBuffer(bytes) {
33
+ return bytes.slice().buffer;
34
+ }
35
+ function assertPassphrase(passphrase) {
36
+ if (typeof passphrase !== 'string') {
37
+ throw new Error(`Use a backup passphrase with at least ${MIN_WORKSPACE_ARCHIVE_PASSPHRASE_CHARACTERS} characters.`);
38
+ }
39
+ if (passphrase.length > MAX_WORKSPACE_ARCHIVE_PASSPHRASE_BYTES) {
40
+ throw new Error('Backup passphrases are limited to 1024 UTF-8 bytes.');
41
+ }
42
+ if (Array.from(passphrase).length < MIN_WORKSPACE_ARCHIVE_PASSPHRASE_CHARACTERS) {
43
+ throw new Error(`Use a backup passphrase with at least ${MIN_WORKSPACE_ARCHIVE_PASSPHRASE_CHARACTERS} characters.`);
44
+ }
45
+ const bytes = encoder.encode(passphrase);
46
+ if (bytes.byteLength > MAX_WORKSPACE_ARCHIVE_PASSPHRASE_BYTES) {
47
+ throw new Error('Backup passphrases are limited to 1024 UTF-8 bytes.');
48
+ }
49
+ return bytes;
50
+ }
51
+ function base64url(bytes) {
52
+ let binary = '';
53
+ for (let index = 0; index < bytes.length; index += 0x8000) {
54
+ binary += String.fromCharCode(...bytes.subarray(index, Math.min(index + 0x8000, bytes.length)));
55
+ }
56
+ return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/u, '');
57
+ }
58
+ function decodeBase64url(value, expectedBytes, label) {
59
+ const expectedCharacters = expectedBytes === null ? null : Math.ceil(expectedBytes * 4 / 3);
60
+ if (typeof value !== 'string'
61
+ || !value
62
+ || (expectedCharacters !== null && value.length > expectedCharacters)
63
+ || !BASE64URL_RE.test(value)) {
64
+ throw new Error(`The encrypted workspace archive has an invalid ${label}.`);
65
+ }
66
+ const padding = (4 - value.length % 4) % 4;
67
+ let binary = '';
68
+ try {
69
+ binary = atob(value.replaceAll('-', '+').replaceAll('_', '/') + '='.repeat(padding));
70
+ }
71
+ catch {
72
+ throw new Error(`The encrypted workspace archive has an invalid ${label}.`);
73
+ }
74
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
75
+ if (expectedBytes !== null && bytes.byteLength !== expectedBytes) {
76
+ throw new Error(`The encrypted workspace archive has an invalid ${label}.`);
77
+ }
78
+ if (base64url(bytes) !== value) {
79
+ throw new Error(`The encrypted workspace archive has a non-canonical ${label}.`);
80
+ }
81
+ return bytes;
82
+ }
83
+ function canonicalMetadata(envelope) {
84
+ return JSON.stringify({
85
+ schema: envelope.schema,
86
+ version: envelope.version,
87
+ createdAt: envelope.createdAt,
88
+ content: envelope.content,
89
+ kdf: envelope.kdf,
90
+ cipher: envelope.cipher,
91
+ });
92
+ }
93
+ function validTimestamp(value) {
94
+ if (typeof value !== 'string' || value.length > 32 || !ISO_TIMESTAMP_RE.test(value))
95
+ return false;
96
+ const timestamp = Date.parse(value);
97
+ return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value;
98
+ }
99
+ function validateEnvelope(raw) {
100
+ const value = record(raw);
101
+ if (!value || value.schema !== ENCRYPTED_WORKSPACE_ARCHIVE_SCHEMA) {
102
+ throw new Error('This file is not an encrypted WHOISleuth workspace archive.');
103
+ }
104
+ if (value.version !== ENCRYPTED_WORKSPACE_ARCHIVE_VERSION) {
105
+ if (typeof value.version === 'number' && Number.isSafeInteger(value.version) && value.version > ENCRYPTED_WORKSPACE_ARCHIVE_VERSION) {
106
+ throw new Error(`This encrypted workspace archive uses newer schema ${value.version}. Update the app before importing it.`);
107
+ }
108
+ throw new Error(`Expected encrypted workspace archive schema ${ENCRYPTED_WORKSPACE_ARCHIVE_VERSION}.`);
109
+ }
110
+ if (!hasExactKeys(value, ['schema', 'version', 'createdAt', 'content', 'kdf', 'cipher', 'ciphertext'])) {
111
+ throw new Error('The encrypted workspace archive envelope is malformed.');
112
+ }
113
+ if (!validTimestamp(value.createdAt)) {
114
+ throw new Error('The encrypted workspace archive has an invalid creation time.');
115
+ }
116
+ const content = record(value.content);
117
+ if (!content
118
+ || !hasExactKeys(content, ['schema', 'version'])
119
+ || content.schema !== WORKSPACE_ARCHIVE_SCHEMA
120
+ || !isSupportedWorkspaceArchiveVersion(content.version)) {
121
+ throw new Error('The encrypted workspace archive declares an unsupported content contract.');
122
+ }
123
+ const kdf = record(value.kdf);
124
+ if (!kdf
125
+ || !hasExactKeys(kdf, ['name', 'hash', 'iterations', 'salt'])
126
+ || kdf.name !== 'PBKDF2'
127
+ || kdf.hash !== 'SHA-256'
128
+ || kdf.iterations !== WORKSPACE_ARCHIVE_PBKDF2_ITERATIONS) {
129
+ throw new Error('The encrypted workspace archive declares an unsupported key-derivation contract.');
130
+ }
131
+ const cipher = record(value.cipher);
132
+ if (!cipher
133
+ || !hasExactKeys(cipher, ['name', 'keyBits', 'tagBits', 'iv'])
134
+ || cipher.name !== 'AES-GCM'
135
+ || cipher.keyBits !== AES_KEY_BITS
136
+ || cipher.tagBits !== AES_GCM_TAG_BITS) {
137
+ throw new Error('The encrypted workspace archive declares an unsupported cipher contract.');
138
+ }
139
+ const salt = decodeBase64url(kdf.salt, SALT_BYTES, 'salt');
140
+ const iv = decodeBase64url(cipher.iv, IV_BYTES, 'initialization vector');
141
+ if (typeof value.ciphertext !== 'string' || value.ciphertext.length > MAX_CIPHERTEXT_BASE64URL_CHARACTERS) {
142
+ throw new Error('The encrypted workspace archive ciphertext exceeds its byte limit.');
143
+ }
144
+ const ciphertext = decodeBase64url(value.ciphertext, null, 'ciphertext');
145
+ if (ciphertext.byteLength < AES_GCM_TAG_BITS / 8 || ciphertext.byteLength > MAX_WORKSPACE_ARCHIVE_BYTES + AES_GCM_TAG_BITS / 8) {
146
+ throw new Error('The encrypted workspace archive ciphertext exceeds its byte limit.');
147
+ }
148
+ return {
149
+ envelope: {
150
+ schema: ENCRYPTED_WORKSPACE_ARCHIVE_SCHEMA,
151
+ version: ENCRYPTED_WORKSPACE_ARCHIVE_VERSION,
152
+ createdAt: value.createdAt,
153
+ content: {
154
+ schema: WORKSPACE_ARCHIVE_SCHEMA,
155
+ version: content.version,
156
+ },
157
+ kdf: {
158
+ name: 'PBKDF2',
159
+ hash: 'SHA-256',
160
+ iterations: WORKSPACE_ARCHIVE_PBKDF2_ITERATIONS,
161
+ salt: kdf.salt,
162
+ },
163
+ cipher: {
164
+ name: 'AES-GCM',
165
+ keyBits: AES_KEY_BITS,
166
+ tagBits: AES_GCM_TAG_BITS,
167
+ iv: cipher.iv,
168
+ },
169
+ ciphertext: value.ciphertext,
170
+ },
171
+ salt,
172
+ iv,
173
+ ciphertext,
174
+ };
175
+ }
176
+ function cryptoProvider(provider = globalThis.crypto) {
177
+ if (!provider
178
+ || typeof provider.getRandomValues !== 'function'
179
+ || typeof provider.subtle?.importKey !== 'function'
180
+ || typeof provider.subtle?.deriveKey !== 'function'
181
+ || typeof provider.subtle?.encrypt !== 'function'
182
+ || typeof provider.subtle?.decrypt !== 'function') {
183
+ throw new Error('Encrypted workspace archives are unavailable in this browser.');
184
+ }
185
+ return provider;
186
+ }
187
+ async function deriveArchiveKey(provider, passphraseBytes, salt, usages) {
188
+ const material = await provider.subtle.importKey('raw', arrayBuffer(passphraseBytes), 'PBKDF2', false, ['deriveKey']);
189
+ return provider.subtle.deriveKey({
190
+ name: 'PBKDF2',
191
+ hash: 'SHA-256',
192
+ salt: arrayBuffer(salt),
193
+ iterations: WORKSPACE_ARCHIVE_PBKDF2_ITERATIONS,
194
+ }, material, { name: 'AES-GCM', length: AES_KEY_BITS }, false, usages);
195
+ }
196
+ export function isEncryptedWorkspaceArchive(raw) {
197
+ return record(raw)?.schema === ENCRYPTED_WORKSPACE_ARCHIVE_SCHEMA;
198
+ }
199
+ /** Validate the bounded envelope without deriving a key or exposing plaintext. */
200
+ export function inspectEncryptedWorkspaceArchive(raw) {
201
+ const { envelope, ciphertext } = validateEnvelope(raw);
202
+ return { createdAt: envelope.createdAt, ciphertextBytes: ciphertext.byteLength };
203
+ }
204
+ /** Encrypt one already-built workspace archive using browser-native Web Crypto. */
205
+ export async function encryptWorkspaceArchive(archive, passphrase, provider) {
206
+ await readWorkspaceArchive(archive);
207
+ const archiveVersion = record(archive)?.version;
208
+ if (!isSupportedWorkspaceArchiveVersion(archiveVersion)) {
209
+ throw new Error('The workspace archive declares an unsupported content contract.');
210
+ }
211
+ const plaintext = JSON.stringify(archive);
212
+ const plaintextBytes = encoder.encode(plaintext);
213
+ if (plaintextBytes.byteLength > MAX_WORKSPACE_ARCHIVE_BYTES) {
214
+ throw new Error('Workspace archives are limited to 10 MiB. Export smaller collections separately before trying again.');
215
+ }
216
+ const crypto = cryptoProvider(provider);
217
+ const passphraseBytes = assertPassphrase(passphrase);
218
+ const salt = crypto.getRandomValues(new Uint8Array(SALT_BYTES));
219
+ const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
220
+ const envelopeWithoutCiphertext = {
221
+ schema: ENCRYPTED_WORKSPACE_ARCHIVE_SCHEMA,
222
+ version: ENCRYPTED_WORKSPACE_ARCHIVE_VERSION,
223
+ createdAt: new Date().toISOString(),
224
+ content: {
225
+ schema: WORKSPACE_ARCHIVE_SCHEMA,
226
+ version: archiveVersion,
227
+ },
228
+ kdf: {
229
+ name: 'PBKDF2',
230
+ hash: 'SHA-256',
231
+ iterations: WORKSPACE_ARCHIVE_PBKDF2_ITERATIONS,
232
+ salt: base64url(salt),
233
+ },
234
+ cipher: {
235
+ name: 'AES-GCM',
236
+ keyBits: AES_KEY_BITS,
237
+ tagBits: AES_GCM_TAG_BITS,
238
+ iv: base64url(iv),
239
+ },
240
+ };
241
+ try {
242
+ const key = await deriveArchiveKey(crypto, passphraseBytes, salt, ['encrypt']);
243
+ const ciphertext = await crypto.subtle.encrypt({
244
+ name: 'AES-GCM',
245
+ iv: arrayBuffer(iv),
246
+ additionalData: arrayBuffer(encoder.encode(canonicalMetadata(envelopeWithoutCiphertext))),
247
+ tagLength: AES_GCM_TAG_BITS,
248
+ }, key, arrayBuffer(plaintextBytes));
249
+ return { ...envelopeWithoutCiphertext, ciphertext: base64url(new Uint8Array(ciphertext)) };
250
+ }
251
+ finally {
252
+ passphraseBytes.fill(0);
253
+ }
254
+ }
255
+ /** Decrypt and parse one encrypted archive. The ordinary archive reader remains authoritative. */
256
+ export async function decryptWorkspaceArchive(raw, passphrase, provider) {
257
+ const { envelope, salt, iv, ciphertext } = validateEnvelope(raw);
258
+ const crypto = cryptoProvider(provider);
259
+ const passphraseBytes = assertPassphrase(passphrase);
260
+ const { ciphertext: _ciphertext, ...metadata } = envelope;
261
+ try {
262
+ const key = await deriveArchiveKey(crypto, passphraseBytes, salt, ['decrypt']);
263
+ const plaintext = await crypto.subtle.decrypt({
264
+ name: 'AES-GCM',
265
+ iv: arrayBuffer(iv),
266
+ additionalData: arrayBuffer(encoder.encode(canonicalMetadata(metadata))),
267
+ tagLength: AES_GCM_TAG_BITS,
268
+ }, key, arrayBuffer(ciphertext));
269
+ if (plaintext.byteLength > MAX_WORKSPACE_ARCHIVE_BYTES) {
270
+ throw new Error('The decrypted workspace archive exceeds its byte limit.');
271
+ }
272
+ return JSON.parse(decoder.decode(plaintext));
273
+ }
274
+ catch (cause) {
275
+ if (cause instanceof Error && cause.message === 'The decrypted workspace archive exceeds its byte limit.')
276
+ throw cause;
277
+ throw new Error('The backup passphrase is incorrect or the encrypted file is corrupted.');
278
+ }
279
+ finally {
280
+ passphraseBytes.fill(0);
281
+ }
282
+ }