@logictan/dsh-config-manager 0.1.61 → 0.1.62
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.
- package/lib/adapters/credentials.d.ts +1 -1
- package/lib/client.d.ts +0 -3
- package/lib/client.js +262 -268
- package/lib/core/analyzer.js +5 -4
- package/lib/core/exporter.d.ts +1 -4
- package/lib/core/exporter.js +14 -45
- package/lib/core/importer.d.ts +1 -1
- package/lib/core/index.d.ts +1 -1
- package/lib/core/messages.d.ts +1 -5
- package/lib/core/messages.js +2 -10
- package/lib/core/types.d.ts +8 -12
- package/lib/index.d.ts +2 -2
- package/lib/index.js +4 -57
- package/lib/security/index.d.ts +1 -3
- package/lib/security/index.js +1 -3
- package/lib/sync/transport.d.ts +2 -2
- package/lib/sync/transport.js +1 -1
- package/lib/ui/i18n.d.ts +0 -3
- package/lib/ui/i18n.js +0 -6
- package/lib/ui/report.js +1 -2
- package/lib/ui/test-helpers.d.ts +0 -6
- package/lib/ui/test-helpers.js +1 -5
- package/lib/ui/types.d.ts +0 -13
- package/package.json +1 -1
- package/src/adapters/credentials.ts +1 -1
- package/src/client/config-manager.module.css +1 -1
- package/src/client/run-store.test.ts +1 -1
- package/src/client/sync/SyncSettingsView.tsx +5 -5
- package/src/client/sync/sync-locales.ts +0 -1
- package/src/core/analyzer.ts +5 -4
- package/src/core/exporter.ts +15 -47
- package/src/core/importer.ts +1 -1
- package/src/core/index.ts +1 -1
- package/src/core/messages.ts +2 -10
- package/src/core/smoke.test.ts +5 -5
- package/src/core/types.ts +8 -12
- package/src/index.ts +5 -56
- package/src/security/index.ts +1 -3
- package/src/security/security.test.ts +0 -361
- package/src/sync/transport.ts +3 -3
- package/src/ui/i18n.ts +0 -6
- package/src/ui/report.ts +1 -2
- package/src/ui/test-helpers.ts +3 -7
- package/src/ui/types.ts +0 -10
- package/lib/security/encryption.d.ts +0 -85
- package/lib/security/encryption.js +0 -279
- package/src/security/encryption.ts +0 -335
|
@@ -16,8 +16,6 @@ import { createLogger, type Logger } from '../utils/logger.ts';
|
|
|
16
16
|
import { zipToBuffer, parseZip, crc32, ZipSafetyError, type ZipWriteEntry } from '../utils/zip.ts';
|
|
17
17
|
import { normalizePath } from '../utils/paths.ts';
|
|
18
18
|
import { sha256Hex } from '../utils/hashing.ts';
|
|
19
|
-
import { CredentialsAdapter } from '../adapters/credentials.ts';
|
|
20
|
-
import { parseManifest } from '../schema/manifest.ts';
|
|
21
19
|
import type {
|
|
22
20
|
ConfigAdapter, CredentialsFacade, ExportSection, FileSystemFacade, HostContext,
|
|
23
21
|
NamespaceInfo, PatchFileFacade, PluginsFacade, SettingsFacade, SnapshotStore,
|
|
@@ -32,12 +30,6 @@ import {
|
|
|
32
30
|
DEFAULT_SECRET_FIELD_NAMES,
|
|
33
31
|
} from './secret-scanner.ts';
|
|
34
32
|
import type { ValuePattern, ConfiguredSecretPatterns } from './secret-scanner.ts';
|
|
35
|
-
import {
|
|
36
|
-
encryptCredentials, decryptCredentials, createEncryptionProvider,
|
|
37
|
-
SecurityError, SCHEMA_MAGIC, SCHEMA_VERSION, HEADER_LENGTH, SALT_LENGTH, IV_LENGTH,
|
|
38
|
-
SCRYPT_PARAMS,
|
|
39
|
-
encryptArchive, decryptArchive, verifyEncryptedBlob, isArchiveBlob, ARCHIVE_MAGIC,
|
|
40
|
-
} from './encryption.ts';
|
|
41
33
|
import {
|
|
42
34
|
buildChecksums, parseChecksumsTable, verifyChecksums, verifyChecksumsJson, describeMismatches,
|
|
43
35
|
} from './integrity.ts';
|
|
@@ -266,158 +258,6 @@ class MiniSettingsAdapter implements ConfigAdapter<SettingsSection> {
|
|
|
266
258
|
async validate() { return { valid: true, issues: [] }; }
|
|
267
259
|
}
|
|
268
260
|
|
|
269
|
-
/* ================= encryption ================= */
|
|
270
|
-
|
|
271
|
-
test('encryption: 往返加密解密一致(scrypt + AES-256-GCM)', async () => {
|
|
272
|
-
const plaintext = 'DEEPSEEK_API_KEY: sk-super-secret-123\nGITHUB_TOKEN: ghp_abcdefghijklmnopqrstuvwxyz\n';
|
|
273
|
-
const { blob, info } = await encryptCredentials(plaintext, 'correct horse battery');
|
|
274
|
-
// blob 布局
|
|
275
|
-
assert.equal(Buffer.from(blob.subarray(0, 4)).toString('ascii'), SCHEMA_MAGIC);
|
|
276
|
-
assert.equal(blob[4], SCHEMA_VERSION);
|
|
277
|
-
assert.equal(blob.length, HEADER_LENGTH + Buffer.byteLength(plaintext, 'utf8'));
|
|
278
|
-
assert.equal(info.algorithm, 'aes-256-gcm');
|
|
279
|
-
assert.equal(info.kdf, 'scrypt');
|
|
280
|
-
assert.deepEqual(info.kdfParams, { ...SCRYPT_PARAMS });
|
|
281
|
-
assert.equal(info.version, 1);
|
|
282
|
-
// 解密往返
|
|
283
|
-
const decrypted = await decryptCredentials(blob, info, 'correct horse battery');
|
|
284
|
-
assert.equal(decrypted, plaintext);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
test('encryption: 错误密码 → BAD_PASSWORD(认证失败,不泄明文)', async () => {
|
|
288
|
-
const { blob, info } = await encryptCredentials('sk-super-secret', 'right-password');
|
|
289
|
-
await assert.rejects(
|
|
290
|
-
() => decryptCredentials(blob, info, 'wrong-password'),
|
|
291
|
-
(err: unknown) => err instanceof SecurityError && err.code === 'BAD_PASSWORD',
|
|
292
|
-
);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
test('encryption: 篡改 manifest 加密参数 → TAMPERED', async () => {
|
|
296
|
-
const { blob, info } = await encryptCredentials('sk-super-secret', 'pw-12345678');
|
|
297
|
-
// 篡改 info.authTag(元数据不一致)
|
|
298
|
-
const tampered = { ...info, authTag: Buffer.from('tampered').toString('base64') };
|
|
299
|
-
await assert.rejects(
|
|
300
|
-
() => decryptCredentials(blob, tampered, 'pw-12345678'),
|
|
301
|
-
(err: unknown) => err instanceof SecurityError && err.code === 'TAMPERED',
|
|
302
|
-
);
|
|
303
|
-
// 篡改 info.salt
|
|
304
|
-
await assert.rejects(
|
|
305
|
-
() => decryptCredentials(blob, { ...info, salt: Buffer.alloc(SALT_LENGTH, 1).toString('base64') }, 'pw-12345678'),
|
|
306
|
-
(err: unknown) => err instanceof SecurityError && err.code === 'TAMPERED',
|
|
307
|
-
);
|
|
308
|
-
// 篡改密文字节 → GCM 认证失败(归为 BAD_PASSWORD:密码错误或密文被改)
|
|
309
|
-
const flipped = Buffer.from(blob);
|
|
310
|
-
flipped[HEADER_LENGTH] = flipped[HEADER_LENGTH]! ^ 0xff;
|
|
311
|
-
await assert.rejects(
|
|
312
|
-
() => decryptCredentials(flipped, info, 'pw-12345678'),
|
|
313
|
-
(err: unknown) => err instanceof SecurityError && err.code === 'BAD_PASSWORD',
|
|
314
|
-
);
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
test('encryption: 截断 blob → TAMPERED;坏 magic → UNSUPPORTED_FORMAT', async () => {
|
|
318
|
-
const { blob, info } = await encryptCredentials('secret', 'pw-12345678');
|
|
319
|
-
await assert.rejects(
|
|
320
|
-
() => decryptCredentials(blob.subarray(0, 10), info, 'pw-12345678'),
|
|
321
|
-
(err: unknown) => err instanceof SecurityError && err.code === 'TAMPERED',
|
|
322
|
-
);
|
|
323
|
-
const badMagic = Buffer.from(blob);
|
|
324
|
-
badMagic.write('XXXX', 0, 'ascii');
|
|
325
|
-
await assert.rejects(
|
|
326
|
-
() => decryptCredentials(badMagic, info, 'pw-12345678'),
|
|
327
|
-
(err: unknown) => err instanceof SecurityError && err.code === 'UNSUPPORTED_FORMAT',
|
|
328
|
-
);
|
|
329
|
-
// kdfParams 被篡改成超大 N(DoS 向量)→ 拒绝
|
|
330
|
-
await assert.rejects(
|
|
331
|
-
() => decryptCredentials(blob, { ...info, kdfParams: { N: 2 ** 24, r: 8, p: 1, keyLength: 32 } }, 'pw-12345678'),
|
|
332
|
-
(err: unknown) => err instanceof SecurityError && err.code === 'UNSUPPORTED_FORMAT',
|
|
333
|
-
);
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
test('encryption: 每次导出 salt/iv 随机;密码绝不出现在 info 与 blob 头', async () => {
|
|
337
|
-
const password = 'my-secret-password-123';
|
|
338
|
-
const r1 = await encryptCredentials('value', password);
|
|
339
|
-
const r2 = await encryptCredentials('value', password);
|
|
340
|
-
assert.notEqual(r1.info.salt, r2.info.salt);
|
|
341
|
-
assert.notEqual(r1.info.iv, r2.info.iv);
|
|
342
|
-
const infoJson = JSON.stringify(r1.info);
|
|
343
|
-
const infoKeys = Object.keys(r1.info);
|
|
344
|
-
assert.ok(!infoKeys.some((k) => k.toLowerCase().includes('password')), 'info 不得含 password 字段');
|
|
345
|
-
assert.ok(!infoJson.includes(password), '密码值不得进入 info');
|
|
346
|
-
assert.ok(!Buffer.from(r1.blob.subarray(0, HEADER_LENGTH)).toString('utf8').includes(password));
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
test('encryption: createEncryptionProvider 对齐 core EncryptionProvider 契约', async () => {
|
|
350
|
-
const provider = createEncryptionProvider('provider-pw-123');
|
|
351
|
-
const plaintext = 'REF: value';
|
|
352
|
-
const { blob, info } = await provider.encrypt(plaintext); // encrypt 无密码参数(闭包持有)
|
|
353
|
-
const decrypted = await provider.decrypt(blob, info, 'provider-pw-123');
|
|
354
|
-
assert.equal(decrypted, plaintext);
|
|
355
|
-
// decrypt 用调用方传入的密码(换密码解密支持)
|
|
356
|
-
await assert.rejects(
|
|
357
|
-
() => provider.decrypt(blob, info, 'another-pw-123'),
|
|
358
|
-
(err: unknown) => err instanceof SecurityError && err.code === 'BAD_PASSWORD',
|
|
359
|
-
);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
/* ---------------- 整体备份容器加密(encryptArchive / verifyEncryptedBlob / decryptArchive) ---------------- */
|
|
366
|
-
|
|
367
|
-
test('archive: 往返加解密一致(任意二进制 ZIP 字节无损)', async () => {
|
|
368
|
-
const zipBytes = Buffer.from('PK\x03\x04this-is-a-real-zip-binary-content\u0000\x01\x02', 'binary');
|
|
369
|
-
const password = 'archive-password-123';
|
|
370
|
-
const { blob } = await encryptArchive(zipBytes, password);
|
|
371
|
-
assert.ok(isArchiveBlob(blob), 'magic DCA1 可探测');
|
|
372
|
-
assert.equal(Buffer.from(blob.subarray(0, 4)).toString('ascii'), ARCHIVE_MAGIC);
|
|
373
|
-
|
|
374
|
-
const verified = await verifyEncryptedBlob(blob, password);
|
|
375
|
-
assert.equal(verified.valid, true);
|
|
376
|
-
assert.equal(verified.ok, true);
|
|
377
|
-
assert.ok(verified.info && verified.kdf, '应返回解密参数');
|
|
378
|
-
|
|
379
|
-
const decrypted = await decryptArchive(blob, verified.info!, verified.kdf!, password);
|
|
380
|
-
assert.deepEqual(Buffer.from(decrypted), zipBytes, '解出的明文 ZIP 字节必须无损一致');
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
test('archive: 密码错误 → BAD_PASSWORD(verify 与 decrypt 都拒绝,不泄明文)', async () => {
|
|
384
|
-
const zipBytes = Buffer.from('PK\x03\x04dsh-config-plaintext');
|
|
385
|
-
const password = 'right-password';
|
|
386
|
-
const { blob } = await encryptArchive(zipBytes, password);
|
|
387
|
-
const verified = await verifyEncryptedBlob(blob, 'wrong-password');
|
|
388
|
-
assert.equal(verified.valid, true);
|
|
389
|
-
assert.equal(verified.ok, false);
|
|
390
|
-
assert.equal(verified.code, 'BAD_PASSWORD');
|
|
391
|
-
await assert.rejects(
|
|
392
|
-
() => decryptArchive(blob, verified.info!, verified.kdf!, 'wrong-password'),
|
|
393
|
-
(err: unknown) => err instanceof SecurityError && err.code === 'BAD_PASSWORD',
|
|
394
|
-
);
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
test('archive: 非容器字节 → valid=false(体积不足判 TAMPERED;magic 不符判 UNSUPPORTED_FORMAT)', async () => {
|
|
398
|
-
const tooShort = Buffer.from('PK\x03\x04plain-zip-not-encrypted');
|
|
399
|
-
assert.equal(isArchiveBlob(tooShort), false);
|
|
400
|
-
const v = await verifyEncryptedBlob(tooShort, 'pw');
|
|
401
|
-
assert.equal(v.valid, false);
|
|
402
|
-
assert.equal(v.code, 'TAMPERED', '体积不足 → TAMPERED');
|
|
403
|
-
|
|
404
|
-
// 长度足够的非容器字节 → magic 不符 → UNSUPPORTED_FORMAT
|
|
405
|
-
const fullLen = Buffer.alloc(64);
|
|
406
|
-
fullLen.write('PK\x03\x04', 0, 'binary');
|
|
407
|
-
const v2 = await verifyEncryptedBlob(fullLen, 'pw');
|
|
408
|
-
assert.equal(v2.valid, false);
|
|
409
|
-
assert.equal(v2.code, 'UNSUPPORTED_FORMAT');
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
test('archive: 篡改密文 → verify ok=false(GCM 认证失败)', async () => {
|
|
413
|
-
const { blob } = await encryptArchive(Buffer.from('sensitive-archive'), 'archive-pw-123');
|
|
414
|
-
const tampered = Buffer.from(blob);
|
|
415
|
-
tampered[tampered.length - 1] = tampered[tampered.length - 1]! ^ 0xff;
|
|
416
|
-
const verified = await verifyEncryptedBlob(tampered, 'archive-pw-123');
|
|
417
|
-
assert.equal(verified.valid, true);
|
|
418
|
-
assert.equal(verified.ok, false, '篡改后密码验证必失败');
|
|
419
|
-
});
|
|
420
|
-
|
|
421
261
|
/* ================= zip-security ================= */
|
|
422
262
|
|
|
423
263
|
test('zip-security: parseZipHardened 兼容 core 正常 ZIP', () => {
|
|
@@ -992,207 +832,6 @@ test('集成: Exporter + createSecretScanner 剥离自定义敏感字段(第
|
|
|
992
832
|
});
|
|
993
833
|
});
|
|
994
834
|
|
|
995
|
-
test('集成: Exporter + EncryptionProvider 加密备份(secrets.enc + manifest + 解密恢复)', async () => {
|
|
996
|
-
await withTmp(async (dir) => {
|
|
997
|
-
const homeDir = path.join(dir, 'home');
|
|
998
|
-
const ctx = new MockHostContext(homeDir);
|
|
999
|
-
ctx.settings.ns.set('general', { value: { theme: 'dark' }, revision: 1, secrets: [] });
|
|
1000
|
-
const credentialsYaml = 'DEEPSEEK_API_KEY: sk-super-secret-value\nGITHUB_TOKEN: ghp_abcdefghijklmnopqrstuvwxyz\n';
|
|
1001
|
-
await ctx.fs.writeFile(path.join(homeDir, '.credentials.yaml'), Buffer.from(credentialsYaml, 'utf8'));
|
|
1002
|
-
|
|
1003
|
-
const adapters: ConfigAdapter[] = [new MiniSettingsAdapter()];
|
|
1004
|
-
const zipPath = path.join(dir, 'enc.zip');
|
|
1005
|
-
const password = 'backup-password-123';
|
|
1006
|
-
const exporter = new Exporter({
|
|
1007
|
-
ctx,
|
|
1008
|
-
adapters,
|
|
1009
|
-
scanner: createSecretScanner(),
|
|
1010
|
-
encryption: createEncryptionProvider(password),
|
|
1011
|
-
now: () => new Date('2026-08-14T12:00:00.000Z'),
|
|
1012
|
-
});
|
|
1013
|
-
const { manifest } = await exporter.export({ includeSecrets: true, outPath: zipPath });
|
|
1014
|
-
|
|
1015
|
-
assert.equal(manifest.security.containsSecrets, true);
|
|
1016
|
-
assert.equal(manifest.security.encrypted, true);
|
|
1017
|
-
assert.ok(manifest.security.encryption, '应记录加密参数');
|
|
1018
|
-
assert.equal(manifest.security.encryption!.kdf, 'scrypt');
|
|
1019
|
-
|
|
1020
|
-
const archive = parseZip(await fs.readFile(zipPath));
|
|
1021
|
-
assert.ok(archive.has('security/secrets.enc'), 'secrets.enc 应写入 ZIP');
|
|
1022
|
-
// 密码绝不出现在 manifest 文本
|
|
1023
|
-
const manifestText = archive.readEntryText('manifest.json');
|
|
1024
|
-
assert.ok(!manifestText.includes(password), '密码不得出现在 manifest');
|
|
1025
|
-
assert.ok(!manifestText.includes('sk-super-secret-value'), '明文秘密不得出现在 manifest');
|
|
1026
|
-
|
|
1027
|
-
// 解密恢复
|
|
1028
|
-
const blob = archive.readEntry('security/secrets.enc');
|
|
1029
|
-
const decrypted = await decryptCredentials(blob, manifest.security.encryption!, password);
|
|
1030
|
-
assert.equal(decrypted, credentialsYaml);
|
|
1031
|
-
});
|
|
1032
|
-
});
|
|
1033
|
-
|
|
1034
|
-
test('集成: 加密备份导入强制密码——无解密结果拒绝,正确解密恢复凭据', async () => {
|
|
1035
|
-
await withTmp(async (dir) => {
|
|
1036
|
-
const homeDir = path.join(dir, 'home');
|
|
1037
|
-
const src = new MockHostContext(homeDir);
|
|
1038
|
-
src.settings.ns.set('general', { value: { theme: 'dark', apiKeyEnv: 'DEEPSEEK_API_KEY' }, revision: 1, secrets: [] });
|
|
1039
|
-
src.credentials.values.set('DEEPSEEK_API_KEY', 'sk-super-secret-value');
|
|
1040
|
-
await src.fs.writeFile(
|
|
1041
|
-
path.join(homeDir, '.credentials.yaml'),
|
|
1042
|
-
Buffer.from('DEEPSEEK_API_KEY: sk-super-secret-value\n', 'utf8'),
|
|
1043
|
-
);
|
|
1044
|
-
const credentialsAdapter = new CredentialsAdapter({ refs: async () => ['DEEPSEEK_API_KEY'] });
|
|
1045
|
-
const adapters: ConfigAdapter[] = [new MiniSettingsAdapter(), credentialsAdapter];
|
|
1046
|
-
const zipPath = path.join(dir, 'enc.zip');
|
|
1047
|
-
const password = 'backup-password-123';
|
|
1048
|
-
await new Exporter({
|
|
1049
|
-
ctx: src,
|
|
1050
|
-
adapters,
|
|
1051
|
-
scanner: createSecretScanner(),
|
|
1052
|
-
encryption: createEncryptionProvider(password),
|
|
1053
|
-
now: () => new Date('2026-08-14T12:00:00.000Z'),
|
|
1054
|
-
}).export({ includeSecrets: true, outPath: zipPath });
|
|
1055
|
-
|
|
1056
|
-
const dst = new MockHostContext(path.join(dir, 'dst-home'));
|
|
1057
|
-
const importer = new Importer({ ctx: dst, adapters, snapshotStore: new MemSnapshotStore() });
|
|
1058
|
-
|
|
1059
|
-
// 分析层必须暴露加密标志(UI 据此要求输入解密密码)
|
|
1060
|
-
const analysis = await importer.analyzeImport(zipPath);
|
|
1061
|
-
assert.equal(analysis.encrypted, true);
|
|
1062
|
-
assert.equal(analysis.secretCount, 1);
|
|
1063
|
-
|
|
1064
|
-
const plan = await importer.createImportPlan(zipPath, { strategy: 'merge', resolutions: {}, pathMappings: [] });
|
|
1065
|
-
assert.ok(plan.missingSecrets.some((s) => s.ref === 'DEEPSEEK_API_KEY'));
|
|
1066
|
-
|
|
1067
|
-
// 1) 无解密结果:加密备份拒绝执行(不允许「无密码照样导入」)
|
|
1068
|
-
await assert.rejects(
|
|
1069
|
-
() => importer.executeImportPlan(zipPath, plan, { confirm: true }),
|
|
1070
|
-
/解密密码才能导入/,
|
|
1071
|
-
);
|
|
1072
|
-
assert.equal(dst.credentials.values.has('DEEPSEEK_API_KEY'), false, '拒绝时不得写入任何凭据');
|
|
1073
|
-
|
|
1074
|
-
// 2) 正确解密结果(宿主用备份密码解开 secrets.enc 注入):凭据恢复,不再要求补录
|
|
1075
|
-
const archive = parseZip(await fs.readFile(zipPath));
|
|
1076
|
-
const manifest = parseManifest(archive.readEntryText('manifest.json'));
|
|
1077
|
-
const blob = archive.readEntry('security/secrets.enc');
|
|
1078
|
-
const plaintext = await decryptCredentials(blob, manifest.security.encryption!, password);
|
|
1079
|
-
const map = new Map<string, string>();
|
|
1080
|
-
for (const line of plaintext.split('\n')) {
|
|
1081
|
-
const m = /^([A-Za-z0-9_]+):\s*(.+)$/.exec(line.trim());
|
|
1082
|
-
if (m) map.set(m[1]!, m[2]!);
|
|
1083
|
-
}
|
|
1084
|
-
const result = await importer.executeImportPlan(zipPath, plan, { confirm: true, decryptedCredentials: map });
|
|
1085
|
-
assert.equal(result.ok, true);
|
|
1086
|
-
assert.equal(result.missingSecrets.length, 0, '解密覆盖的凭据不再计入缺失');
|
|
1087
|
-
assert.equal(dst.credentials.values.get('DEEPSEEK_API_KEY'), 'sk-super-secret-value');
|
|
1088
|
-
});
|
|
1089
|
-
});
|
|
1090
|
-
|
|
1091
|
-
test('集成: includeSecrets 无加密提供者 → 拒绝(绝不明文导出秘密)', async () => {
|
|
1092
|
-
await withTmp(async (dir) => {
|
|
1093
|
-
const ctx = new MockHostContext(path.join(dir, 'home'));
|
|
1094
|
-
ctx.settings.ns.set('general', { value: { theme: 'dark' }, revision: 1, secrets: [] });
|
|
1095
|
-
const exporter = new Exporter({ ctx, adapters: [new MiniSettingsAdapter()], now: () => new Date() });
|
|
1096
|
-
await assert.rejects(
|
|
1097
|
-
() => exporter.export({ includeSecrets: true, outPath: path.join(dir, 'x.zip') }),
|
|
1098
|
-
/EncryptionProvider/,
|
|
1099
|
-
);
|
|
1100
|
-
});
|
|
1101
|
-
});
|
|
1102
|
-
|
|
1103
|
-
test('集成: 只加密不导出密钥(encryption 提供但 includeSecrets=false)→ 备份仍标记加密,但不含任何凭据值', async () => {
|
|
1104
|
-
await withTmp(async (dir) => {
|
|
1105
|
-
const homeDir = path.join(dir, 'home');
|
|
1106
|
-
const ctx = new MockHostContext(homeDir);
|
|
1107
|
-
ctx.settings.ns.set('general', { value: { theme: 'dark' }, revision: 1, secrets: [] });
|
|
1108
|
-
// 存在真实凭据文件,但用户只勾了「加密备份」、未勾「导出密钥」—— 凭据值绝不能进备份
|
|
1109
|
-
const credentialsYaml = 'DEEPSEEK_API_KEY: sk-super-secret-value\n';
|
|
1110
|
-
await ctx.fs.writeFile(path.join(homeDir, '.credentials.yaml'), Buffer.from(credentialsYaml, 'utf8'));
|
|
1111
|
-
|
|
1112
|
-
const adapters: ConfigAdapter[] = [new MiniSettingsAdapter()];
|
|
1113
|
-
const zipPath = path.join(dir, 'enc-only.zip');
|
|
1114
|
-
const password = 'backup-password-123';
|
|
1115
|
-
const exporter = new Exporter({
|
|
1116
|
-
ctx,
|
|
1117
|
-
adapters,
|
|
1118
|
-
scanner: createSecretScanner(),
|
|
1119
|
-
encryption: createEncryptionProvider(password),
|
|
1120
|
-
now: () => new Date('2026-08-14T12:00:00.000Z'),
|
|
1121
|
-
});
|
|
1122
|
-
const { manifest } = await exporter.export({ includeSecrets: false, outPath: zipPath });
|
|
1123
|
-
|
|
1124
|
-
assert.equal(manifest.security.encrypted, true, '加密是独立选项:不含密钥也应标记加密');
|
|
1125
|
-
assert.equal(manifest.security.containsSecrets, false, '未导出密钥:不得声称包含秘密');
|
|
1126
|
-
assert.ok(manifest.security.encryption, '应记录加密参数');
|
|
1127
|
-
|
|
1128
|
-
const archive = parseZip(await fs.readFile(zipPath));
|
|
1129
|
-
assert.ok(archive.has('security/secrets.enc'), 'secrets.enc 应写入 ZIP');
|
|
1130
|
-
const blob = archive.readEntry('security/secrets.enc');
|
|
1131
|
-
const decrypted = await decryptCredentials(blob, manifest.security.encryption!, password);
|
|
1132
|
-
assert.equal(decrypted, '', '未导出密钥时 secrets.enc 解密内容必须为空');
|
|
1133
|
-
for (const name of archive.names()) {
|
|
1134
|
-
if (name === 'security/secrets.enc') continue // secrets.enc 为二进制加密内容,单独断言解密结果
|
|
1135
|
-
assert.ok(
|
|
1136
|
-
!archive.readEntryText(name).includes('sk-super-secret-value'),
|
|
1137
|
-
`凭据值绝不得出现在备份条目 ${name} 中`,
|
|
1138
|
-
)
|
|
1139
|
-
}
|
|
1140
|
-
});
|
|
1141
|
-
});
|
|
1142
|
-
|
|
1143
|
-
test('集成: 整体加密备份容器——导出→容器→解锁→明文 ZIP→分析(完整链路)', async () => {
|
|
1144
|
-
await withTmp(async (dir) => {
|
|
1145
|
-
const homeDir = path.join(dir, 'home');
|
|
1146
|
-
const ctx = new MockHostContext(homeDir);
|
|
1147
|
-
ctx.settings.ns.set('general', { value: { theme: 'dark' }, revision: 1, secrets: [] });
|
|
1148
|
-
await ctx.fs.writeFile(
|
|
1149
|
-
path.join(homeDir, '.credentials.yaml'),
|
|
1150
|
-
Buffer.from('DEEPSEEK_API_KEY: sk-super-secret-value\n', 'utf8'),
|
|
1151
|
-
);
|
|
1152
|
-
const adapters: ConfigAdapter[] = [new MiniSettingsAdapter()];
|
|
1153
|
-
const password = 'archive-whole-backup-pw';
|
|
1154
|
-
|
|
1155
|
-
// 1) Exporter 生成明文 ZIP(注入 EncryptionProvider 以便 includeSecrets 在 ZIP 内产出 secrets.enc;
|
|
1156
|
-
// 最终整体加密由外层容器完成)
|
|
1157
|
-
const plainZip = path.join(dir, 'plain.zip');
|
|
1158
|
-
await new Exporter({
|
|
1159
|
-
ctx,
|
|
1160
|
-
adapters,
|
|
1161
|
-
encryption: createEncryptionProvider(password),
|
|
1162
|
-
now: () => new Date(),
|
|
1163
|
-
}).export({ includeSecrets: true, outPath: plainZip });
|
|
1164
|
-
|
|
1165
|
-
// 2) 整体加密为容器
|
|
1166
|
-
const rawZip = await fs.readFile(plainZip);
|
|
1167
|
-
const { blob } = await encryptArchive(rawZip, password);
|
|
1168
|
-
const containerPath = path.join(dir, 'backup.zip');
|
|
1169
|
-
await fs.writeFile(containerPath, blob);
|
|
1170
|
-
assert.ok(isArchiveBlob(await fs.readFile(containerPath)), '落盘文件是加密容器(magic DCA1)');
|
|
1171
|
-
|
|
1172
|
-
// 3) 解锁(只读校验 + 解密)→ 明文 ZIP
|
|
1173
|
-
const containerBytes = await fs.readFile(containerPath);
|
|
1174
|
-
const verified = await verifyEncryptedBlob(containerBytes, password);
|
|
1175
|
-
assert.equal(verified.valid, true);
|
|
1176
|
-
assert.equal(verified.ok, true, '正确密码应通过验证');
|
|
1177
|
-
const plain = await decryptArchive(containerBytes, verified.info!, verified.kdf!, password);
|
|
1178
|
-
const unlockedPath = path.join(dir, 'unlocked.zip');
|
|
1179
|
-
await fs.writeFile(unlockedPath, plain);
|
|
1180
|
-
|
|
1181
|
-
// 4) 明文 ZIP 可被 Importer 正常分析
|
|
1182
|
-
const importer = new Importer({ ctx, adapters, snapshotStore: new MemSnapshotStore() });
|
|
1183
|
-
const analysis = await importer.analyzeImport(unlockedPath);
|
|
1184
|
-
assert.equal(analysis.valid, true);
|
|
1185
|
-
assert.deepEqual(analysis.sectionsInZip, ['settings']);
|
|
1186
|
-
|
|
1187
|
-
// 5) 明文 ZIP 中确实包含完整凭据(includeSecrets=true 时 Exporter 在 ZIP 内写入 secrets.enc)
|
|
1188
|
-
const archive = parseZip(plain);
|
|
1189
|
-
assert.ok(archive.has('security/secrets.enc'), '容器解出的 ZIP 含 secrets.enc(凭据受容器整体保护)');
|
|
1190
|
-
// 错误密码:verify 拒绝
|
|
1191
|
-
const wrong = await verifyEncryptedBlob(containerBytes, 'wrong-password');
|
|
1192
|
-
assert.equal(wrong.ok, false);
|
|
1193
|
-
});
|
|
1194
|
-
});
|
|
1195
|
-
|
|
1196
835
|
test('集成: Importer + parseZipOverride=createHardenedZipParser 正常解析备份', async () => {
|
|
1197
836
|
await withTmp(async (dir) => {
|
|
1198
837
|
const homeDir = path.join(dir, 'home');
|
package/src/sync/transport.ts
CHANGED
|
@@ -33,9 +33,9 @@ export interface SyncSnapshotMeta {
|
|
|
33
33
|
/** 加密快照的 sections 载荷:整个明文 sections 对象序列化后整体加密(AES-256-GCM)。 */
|
|
34
34
|
export interface EncryptedSections {
|
|
35
35
|
encrypted: {
|
|
36
|
-
/** 加密参数(salt/iv/authTag base64
|
|
36
|
+
/** 加密参数(salt/iv/authTag base64);本插件已不生成,仅供读取历史快照 */
|
|
37
37
|
info: EncryptionInfo;
|
|
38
|
-
/** base64
|
|
38
|
+
/** base64 密文(明文 = 序列化的 sections Record) */
|
|
39
39
|
data: string;
|
|
40
40
|
};
|
|
41
41
|
}
|
|
@@ -87,7 +87,7 @@ export function computeSnapshotMeta(snapshot: SyncSnapshot): SyncSnapshotMeta {
|
|
|
87
87
|
export function sectionsEqual(remote: SyncSnapshotMeta, local: SyncSnapshotMeta): boolean {
|
|
88
88
|
const r = remote.sections;
|
|
89
89
|
const l = local.sections;
|
|
90
|
-
if (Object.keys(l).length === 0) return false; //
|
|
90
|
+
if (Object.keys(l).length === 0) return false; // 本地无分区(空快照)→ 无法比较
|
|
91
91
|
if (Object.keys(r).length !== Object.keys(l).length) return false;
|
|
92
92
|
for (const key of Object.keys(r)) {
|
|
93
93
|
if (r[key as SectionId] !== l[key as SectionId]) return false;
|
package/src/ui/i18n.ts
CHANGED
|
@@ -39,11 +39,9 @@ export const uiZh = {
|
|
|
39
39
|
'report.security': '安全:',
|
|
40
40
|
'report.apiKeysExcluded': 'API 密钥已排除:',
|
|
41
41
|
'report.containsSecrets': '包含密钥:',
|
|
42
|
-
'report.encrypted': '已加密:',
|
|
43
42
|
'report.redacted': '{count} 个敏感字段已脱敏',
|
|
44
43
|
'report.file': '文件:',
|
|
45
44
|
'report.yes': '是',
|
|
46
|
-
'report.yesEncrypted': '是(加密)',
|
|
47
45
|
'report.no': '否',
|
|
48
46
|
'report.importedRestored': '已导入/恢复',
|
|
49
47
|
'report.skipped': '跳过',
|
|
@@ -159,7 +157,6 @@ export const uiZh = {
|
|
|
159
157
|
'sync.pushOk': '推送成功(快照 {id})',
|
|
160
158
|
'sync.pushPreviewHeadline': '将推送 {total} 个分区({changed} 个有变化)',
|
|
161
159
|
'sync.pushPreviewHint': '以上为只读预览,不会写入远端。确认后点击「推送」才真正上传。',
|
|
162
|
-
'sync.pushPreviewEncrypted': '加密快照:载荷将整体加密,各分区相对基线的变化不可比对。',
|
|
163
160
|
'sync.pullFailed': '拉取失败',
|
|
164
161
|
'sync.pullOk': '远端快照 {id} 差异预览:共 {count} 项变更',
|
|
165
162
|
'sync.pullEmpty': '远端快照与本地一致(无变更)',
|
|
@@ -324,11 +321,9 @@ export const uiEn: Record<UiTextKey, string> = {
|
|
|
324
321
|
'report.security': 'Security:',
|
|
325
322
|
'report.apiKeysExcluded': 'API Keys excluded:',
|
|
326
323
|
'report.containsSecrets': 'Contains secrets:',
|
|
327
|
-
'report.encrypted': 'Encrypted:',
|
|
328
324
|
'report.redacted': '{count} sensitive field(s) redacted',
|
|
329
325
|
'report.file': 'File:',
|
|
330
326
|
'report.yes': 'yes',
|
|
331
|
-
'report.yesEncrypted': 'yes (encrypted)',
|
|
332
327
|
'report.no': 'no',
|
|
333
328
|
'report.importedRestored': 'imported/restored',
|
|
334
329
|
'report.skipped': 'skipped',
|
|
@@ -440,7 +435,6 @@ export const uiEn: Record<UiTextKey, string> = {
|
|
|
440
435
|
'sync.pushOk': 'Push succeeded (snapshot {id})',
|
|
441
436
|
'sync.pushPreviewHeadline': 'Will push {total} section(s) ({changed} changed)',
|
|
442
437
|
'sync.pushPreviewHint': 'Read-only preview above — nothing is written to the remote. Click "Push" to actually upload.',
|
|
443
|
-
'sync.pushPreviewEncrypted': 'Encrypted snapshot: the payload is encrypted as a whole; per-section changes vs the baseline cannot be compared.',
|
|
444
438
|
'sync.pullFailed': 'Pull failed',
|
|
445
439
|
'sync.pullOk': 'Remote snapshot {id} diff preview: {count} change(s)',
|
|
446
440
|
'sync.pullEmpty': 'Remote snapshot matches local (no changes)',
|
package/src/ui/report.ts
CHANGED
|
@@ -30,8 +30,7 @@ export function renderExportReport(report: ExportReport, t: UiT = zhUiT): string
|
|
|
30
30
|
}
|
|
31
31
|
lines.push(t('report.security'));
|
|
32
32
|
lines.push(` ✓ ${t('report.apiKeysExcluded')} ${report.security.secretsExcluded ? t('report.yes') : t('report.no')}`);
|
|
33
|
-
lines.push(` ✓ ${t('report.containsSecrets')} ${report.security.containsSecrets ? t('report.
|
|
34
|
-
lines.push(` ✓ ${t('report.encrypted')} ${report.security.encrypted ? t('report.yes') : t('report.no')}`);
|
|
33
|
+
lines.push(` ✓ ${t('report.containsSecrets')} ${report.security.containsSecrets ? t('report.yes') : t('report.no')}`);
|
|
35
34
|
if (report.security.redactedHits > 0) lines.push(` ⚠ ${t('report.redacted', { count: String(report.security.redactedHits) })}`);
|
|
36
35
|
lines.push('');
|
|
37
36
|
lines.push(`${t('report.file')} ${report.file.name} (${formatBytes(report.file.sizeBytes)})`);
|
package/src/ui/test-helpers.ts
CHANGED
|
@@ -33,7 +33,7 @@ export function makeExportReport(overrides: Partial<ExportReport> = {}): ExportR
|
|
|
33
33
|
{ section: 'plugins', counts: { plugins: 8 } },
|
|
34
34
|
],
|
|
35
35
|
excluded: ['sessions', 'pluginFiles'],
|
|
36
|
-
security: { secretsExcluded: true, containsSecrets: false,
|
|
36
|
+
security: { secretsExcluded: true, containsSecrets: false, redactedHits: 2 },
|
|
37
37
|
file: { name: 'dsh-config-2026-08-14.zip', sizeBytes: 20480 },
|
|
38
38
|
warnings: [],
|
|
39
39
|
...overrides,
|
|
@@ -149,7 +149,7 @@ export class MockImportPort implements ImportPort {
|
|
|
149
149
|
result: ImportResult;
|
|
150
150
|
analyzeCalls = 0;
|
|
151
151
|
planCalls: ImportDecisions[] = [];
|
|
152
|
-
executeCalls: { confirm: boolean; secretInputs?: Record<string, string>; rollbackOnError: boolean;
|
|
152
|
+
executeCalls: { confirm: boolean; secretInputs?: Record<string, string>; rollbackOnError: boolean; plan?: ImportPlan }[] = [];
|
|
153
153
|
|
|
154
154
|
constructor(opts: {
|
|
155
155
|
analysis?: ImportAnalysis;
|
|
@@ -169,14 +169,10 @@ export class MockImportPort implements ImportPort {
|
|
|
169
169
|
this.planCalls.push(decisions);
|
|
170
170
|
return this.plan;
|
|
171
171
|
}
|
|
172
|
-
async decryptArchive(zipPath: string): Promise<{ zipPath: string; refs: string[] }> {
|
|
173
|
-
// 测试用:把传入路径视为已解锁的明文 ZIP(不真正解密),无内部凭据
|
|
174
|
-
return { zipPath, refs: [] };
|
|
175
|
-
}
|
|
176
172
|
async executeImportPlan(
|
|
177
173
|
_zip: string,
|
|
178
174
|
plan: ImportPlan,
|
|
179
|
-
opts: { confirm: boolean; secretInputs?: Record<string, string>; rollbackOnError: boolean
|
|
175
|
+
opts: { confirm: boolean; secretInputs?: Record<string, string>; rollbackOnError: boolean },
|
|
180
176
|
): Promise<ImportResult> {
|
|
181
177
|
this.executeCalls.push({ ...opts, plan });
|
|
182
178
|
return this.result;
|
package/src/ui/types.ts
CHANGED
|
@@ -175,14 +175,6 @@ export interface RollbackView {
|
|
|
175
175
|
export interface ImportPort {
|
|
176
176
|
analyzeImport(zipPath: string): Promise<ImportAnalysis>;
|
|
177
177
|
createImportPlan(zipPath: string, decisions: ImportDecisions): Promise<ImportPlan>;
|
|
178
|
-
/**
|
|
179
|
-
* 解锁整体加密备份(只读,零写入):用备份密码解密上传的加密容器,得到明文 ZIP
|
|
180
|
-
* 写入受控临时目录并返回新的 zipPath,供 analyze/plan/execute 引用。
|
|
181
|
-
* 顺带返回解密覆盖的凭据 ref 名(非值)——导出时容器密码与内部 secrets.enc
|
|
182
|
-
* 密码同源,解锁即完成凭据解密验证,无需第二次密码校验。
|
|
183
|
-
* 密码仅内存,绝不落盘/落日志;解密后的明文 ZIP 亦为临时文件,导入结束后清理。
|
|
184
|
-
*/
|
|
185
|
-
decryptArchive(zipPath: string, password: string): Promise<{ zipPath: string; refs: string[] }>;
|
|
186
178
|
executeImportPlan(
|
|
187
179
|
zipPath: string,
|
|
188
180
|
plan: ImportPlan,
|
|
@@ -192,8 +184,6 @@ export interface ImportPort {
|
|
|
192
184
|
secretInputs?: Record<string, string>;
|
|
193
185
|
/** 显式回滚策略:true=任一项失败整体回滚(场景 E);false=单项失败继续(§34.17) */
|
|
194
186
|
rollbackOnError: boolean;
|
|
195
|
-
/** 加密备份的解密密码(仅内存;core 拒绝加密备份无密码执行) */
|
|
196
|
-
decryptPassword?: string;
|
|
197
187
|
},
|
|
198
188
|
): Promise<ImportResult>;
|
|
199
189
|
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import type { EncryptionInfo } from '../schema/types.ts';
|
|
2
|
-
import type { EncryptionProvider } from '../core/types.ts';
|
|
3
|
-
export declare const SCHEMA_MAGIC = "DSC1";
|
|
4
|
-
export declare const SCHEMA_VERSION = 1;
|
|
5
|
-
/** 整体加密备份容器的 magic(D C A rchive):头部布局同 secrets.enc,kdf 参数用默认常量 */
|
|
6
|
-
export declare const ARCHIVE_MAGIC = "DCA1";
|
|
7
|
-
export declare const ARCHIVE_VERSION = 1;
|
|
8
|
-
/** 探测字节是否为整体加密备份容器(上传/下载时判定 containerType) */
|
|
9
|
-
export declare function isArchiveBlob(buf: Uint8Array): boolean;
|
|
10
|
-
export declare const SCRYPT_PARAMS: {
|
|
11
|
-
readonly N: 16384;
|
|
12
|
-
readonly r: 8;
|
|
13
|
-
readonly p: 1;
|
|
14
|
-
readonly keyLength: 32;
|
|
15
|
-
};
|
|
16
|
-
export declare const SALT_LENGTH = 16;
|
|
17
|
-
export declare const IV_LENGTH = 12;
|
|
18
|
-
export declare const TAG_LENGTH = 16;
|
|
19
|
-
/** magic(4) + version(1) + salt(16) + iv(12) + authTag(16) */
|
|
20
|
-
export declare const HEADER_LENGTH: number;
|
|
21
|
-
export type SecurityErrorCode = 'BAD_PASSWORD' | 'TAMPERED' | 'UNSUPPORTED_FORMAT';
|
|
22
|
-
export declare class SecurityError extends Error {
|
|
23
|
-
readonly code: SecurityErrorCode;
|
|
24
|
-
constructor(code: SecurityErrorCode, message: string);
|
|
25
|
-
}
|
|
26
|
-
/** KDF 参数值域校验(防 manifest 被篡改成超大 N 导致 DoS;非法即拒绝) */
|
|
27
|
-
export declare function validateKdfParams(params: unknown): params is EncryptionInfo['kdfParams'];
|
|
28
|
-
/** 派生密钥(scrypt,参数可来自 manifest;默认常量) */
|
|
29
|
-
export declare function deriveKey(password: string, salt: Uint8Array, params?: EncryptionInfo['kdfParams']): Promise<Buffer>;
|
|
30
|
-
/** 加密 .credentials.yaml 原文 → { blob, info }(info 直接进 manifest.security.encryption) */
|
|
31
|
-
export declare function encryptCredentials(plaintext: string, password: string): Promise<{
|
|
32
|
-
blob: Uint8Array;
|
|
33
|
-
info: EncryptionInfo;
|
|
34
|
-
}>;
|
|
35
|
-
/** 解密 secrets.enc(authTag 校验失败抛 BAD_PASSWORD;元数据不一致抛 TAMPERED) */
|
|
36
|
-
export declare function decryptCredentials(blob: Uint8Array, info: EncryptionInfo, password: string): Promise<string>;
|
|
37
|
-
/**
|
|
38
|
-
* 创建 core `EncryptionProvider`(对齐 core/types.ts 契约)。
|
|
39
|
-
* encrypt 使用闭包持有密码;decrypt 使用调用方传入的密码(支持换密码解密)。
|
|
40
|
-
*/
|
|
41
|
-
export declare function createEncryptionProvider(password: string): EncryptionProvider;
|
|
42
|
-
/**
|
|
43
|
-
* 加密完整 ZIP 字节 → 加密容器 blob。
|
|
44
|
-
*
|
|
45
|
-
* 布局(DCA1,与 secrets.enc 同构,kdf 参数用默认常量):
|
|
46
|
-
* magic "DCA1"(4B) + version(1B) + salt(16B) + iv(12B) + authTag(16B) + ciphertext(plainZIP)
|
|
47
|
-
*
|
|
48
|
-
* 语义:整个备份(manifest / 各分区 JSON / sessions / 插件文件 / secrets.enc……)
|
|
49
|
-
* 全部进入 ciphertext,磁盘上无法看到任何明文内容。verifyEncryptedBlob 用于
|
|
50
|
-
* 校验密码正确(GCM authTag 通过)且返回真实解密用的随机参数。
|
|
51
|
-
*/
|
|
52
|
-
export declare function encryptArchive(plainZip: Uint8Array, password: string): Promise<{
|
|
53
|
-
blob: Uint8Array;
|
|
54
|
-
info: EncryptionInfo;
|
|
55
|
-
kdf: {
|
|
56
|
-
salt: Buffer;
|
|
57
|
-
iv: Buffer;
|
|
58
|
-
};
|
|
59
|
-
}>;
|
|
60
|
-
/** 容器版解密:用「先验真实参数」校验密码并解密(—— 调用方必须先用 verifyEncryptedBlob 拿到 info/kdf) */
|
|
61
|
-
export declare function decryptArchive(blob: Uint8Array, info: EncryptionInfo, kdfParam: {
|
|
62
|
-
salt: Buffer;
|
|
63
|
-
iv: Buffer;
|
|
64
|
-
}, password: string): Promise<Uint8Array>;
|
|
65
|
-
export interface EncryptedBlobInfo {
|
|
66
|
-
/** 容器是否合法(magic/version/header 校验通过) */
|
|
67
|
-
valid: boolean;
|
|
68
|
-
/** 密码是否正确(GCM authTag 认证通过);valid=false 时恒 false */
|
|
69
|
-
ok: boolean;
|
|
70
|
-
/** 解密所需参数(valid && ok 时有效;用默认常量派生 key) */
|
|
71
|
-
info: EncryptionInfo | null;
|
|
72
|
-
kdf: {
|
|
73
|
-
salt: Buffer;
|
|
74
|
-
iv: Buffer;
|
|
75
|
-
} | null;
|
|
76
|
-
/** 错误码(供上层转用户可读错误):BAD_PASSWORD / TAMPERED / UNSUPPORTED_FORMAT */
|
|
77
|
-
code: SecurityErrorCode | null;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* 校验加密备份容器并验证密码(只读,零解密返回内容)。
|
|
81
|
-
* - 容器不合法(非 DCA1 / 截断 / 参数异常)→ valid=false + code
|
|
82
|
-
* - 密码正确 → ok=true + 返回随机参数(后续 decryptArchive 用之)
|
|
83
|
-
* - 密码错误 → ok=false + code=BAD_PASSWORD
|
|
84
|
-
*/
|
|
85
|
-
export declare function verifyEncryptedBlob(blob: Uint8Array, password: string): Promise<EncryptedBlobInfo>;
|