import test from 'node:test'; import assert from 'node:assert/strict'; import crypto from 'node:crypto'; import { sealCredential, sha256 } from '../scripts/lib/workshop-credential-envelope.mjs'; import { bindingUpdate } from '../scripts/restore-workshop-credential-binding.mjs'; import { decryptEnvelope } from '../docs/workshop-auth-recovery-20261108/receipts/r1-return/files/public-recipient/decrypt-module.mjs'; // Test-only keys and token. Production recipient private material is never needed here. const pair = crypto.generateKeyPairSync('rsa', { modulusLength: 3072 }); const publicKey = pair.publicKey.export({ type: 'spki', format: 'pem' }); const env = { KSF_FUNCTIONS_URL: 'https://kangshifu.fmode.cn/api/functions', KSF_PARSE_APP_ID: 'FKieRbIoGt_main', KSF_SERVICE_TOKEN: 'T'.repeat(43) }; const parameters = { publicKey, host: '0123456789ab', directory: '/opt/data/ksf-key-receive-20261031-test', credentialId: 'test-credential', expiresAt: '2030-01-02T00:00:00.000Z', env, now: Date.parse('2030-01-01T00:00:00Z') }; const context = { host: parameters.host, directory: parameters.directory, publicKeySha256: sha256(pair.publicKey.export({ type: 'spki', format: 'der' })), now: parameters.now }; test('original returned decryptor accepts the renewed envelope and preserves exact identity/config', () => { const envelope = sealCredential(parameters); const result = decryptEnvelope(envelope, pair.privateKey, context); assert.equal(result.credentialId, parameters.credentialId); assert.equal(result.expiresAt, parameters.expiresAt); assert.deepEqual(result.env, env); assert.equal(JSON.stringify(envelope).includes(env.KSF_SERVICE_TOKEN), false); }); test('original decryptor rejects expired credentials even if encryption itself is intact', () => { const envelope = sealCredential(parameters); assert.throws(() => decryptEnvelope(envelope, pair.privateKey, { ...context, now: Date.parse(parameters.expiresAt) }), { code: 'CREDENTIAL_EXPIRED_OR_MISMATCH' }); }); test('recipient host and key binding are enforced by the unchanged decryptor', () => { const envelope = sealCredential(parameters); assert.throws(() => decryptEnvelope(envelope, pair.privateKey, { ...context, host: 'different-host' }), { code: 'WRONG_RECIPIENT' }); assert.throws(() => decryptEnvelope(envelope, pair.privateKey, { ...context, publicKeySha256: '0'.repeat(64) }), { code: 'KEY_FINGERPRINT_MISMATCH' }); }); test('expiry cannot be extended by editing only the public header', () => { const envelope = sealCredential(parameters); envelope.header.expiresAt = '2040-01-01T00:00:00Z'; assert.throws(() => decryptEnvelope(envelope, pair.privateKey, context), { code: 'HEADER_INCONSISTENT' }); envelope.aadBase64 = Buffer.from(JSON.stringify(envelope.header)).toString('base64'); assert.throws(() => decryptEnvelope(envelope, pair.privateKey, context)); }); test('ciphertext tampering is rejected and encryption uses fresh random material', () => { const first = sealCredential(parameters), second = sealCredential(parameters); assert.notEqual(first.ciphertextBase64, second.ciphertextBase64); const bytes = Buffer.from(first.ciphertextBase64, 'base64'); bytes[0] ^= 1; first.ciphertextBase64 = bytes.toString('base64'); assert.throws(() => decryptEnvelope(first, pair.privateKey, context)); }); test('sealer rejects wrong endpoint, extra credential keys, weak public keys and expired input', () => { assert.throws(() => sealCredential({ ...parameters, env: { ...env, MASTER_KEY: 'not-allowed' } }), { code: 'ENV_KEYS_NOT_ALLOWED' }); assert.throws(() => sealCredential({ ...parameters, env: { ...env, KSF_FUNCTIONS_URL: 'https://other.example/api/functions' } }), { code: 'BAD_SERVICE_CONFIG' }); assert.throws(() => sealCredential({ ...parameters, now: Date.parse(parameters.expiresAt) }), { code: 'EXPIRED_CREDENTIAL' }); const weak = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 }).publicKey.export({ type: 'spki', format: 'pem' }); assert.throws(() => sealCredential({ ...parameters, publicKey: weak }), { code: 'BAD_PUBLIC_KEY' }); }); const binding = { version: 'ksf.encrypted-entry.v1', purpose: 'KSF-credential-receive-20261031', host: parameters.host, keyDirectory: parameters.directory, publicKeySha256: context.publicKeySha256, decryptModule: '/opt/data/receiver.mjs', decryptModuleSha256: 'a'.repeat(64), envelopeFile: '/opt/data/old.json', envelopeSha256: 'b'.repeat(64), preservedExtra: 'keep' }; const bindingBytes = Buffer.from(JSON.stringify(binding)); const release = { ...binding, expectedBindingSha256: sha256(bindingBytes), oldEnvelopeFile: binding.envelopeFile, oldEnvelopeSha256: binding.envelopeSha256, installEnvelope: '/opt/data/new.json', envelopeSha256: 'c'.repeat(64) }; test('binding update changes only the two envelope fields and supports an identical replay', () => { const plan = bindingUpdate(bindingBytes, release, parameters.host); assert.equal(plan.alreadyApplied, false); assert.deepEqual(plan.after, { ...binding, envelopeFile: release.installEnvelope, envelopeSha256: release.envelopeSha256 }); const replay = bindingUpdate(plan.afterBytes, release, parameters.host); assert.equal(replay.alreadyApplied, true); assert.deepEqual(replay.afterBytes, plan.afterBytes); }); test('binding changes, other recipient hosts and key substitutions are blocked', () => { assert.throws(() => bindingUpdate(Buffer.from(JSON.stringify({ ...binding, preservedExtra: 'concurrent' })), release, parameters.host), { code: 'UNKNOWN_BINDING_VERSION' }); assert.throws(() => bindingUpdate(bindingBytes, release, 'other-container'), { code: 'WRONG_CONTAINER' }); assert.throws(() => bindingUpdate(bindingBytes, { ...release, publicKeySha256: 'f'.repeat(64) }, parameters.host), { code: 'RECIPIENT_BINDING_CHANGED' }); });