workshop-credential-envelope.mjs 2.4 KB

12345678910111213141516171819202122232425262728293031323334
  1. import crypto from 'node:crypto';
  2. export const PURPOSE = 'KSF-credential-receive-20261031';
  3. export const sha256 = value => crypto.createHash('sha256').update(value).digest('hex');
  4. const must = (value, code) => { if (!value) throw Object.assign(new Error(code), { code }); };
  5. export function sealCredential({ publicKey, host, directory, credentialId, expiresAt, env, now = Date.now() }) {
  6. const recipient = crypto.createPublicKey(publicKey);
  7. must(recipient.asymmetricKeyType === 'rsa' && recipient.asymmetricKeyDetails.modulusLength >= 3072, 'BAD_PUBLIC_KEY');
  8. must(typeof host === 'string' && /^[a-f0-9]{12,64}$/.test(host), 'BAD_RECIPIENT_HOST');
  9. must(/^\/opt\/data\/ksf-key-receive-20261031-[A-Za-z0-9]+$/.test(directory), 'BAD_KEY_DIRECTORY');
  10. must(Number.isFinite(Date.parse(expiresAt)) && Date.parse(expiresAt) > now, 'EXPIRED_CREDENTIAL');
  11. must(typeof credentialId === 'string' && credentialId.length > 0, 'MISSING_CREDENTIAL_ID');
  12. must(env && Object.keys(env).sort().join(',') === 'KSF_FUNCTIONS_URL,KSF_PARSE_APP_ID,KSF_SERVICE_TOKEN', 'ENV_KEYS_NOT_ALLOWED');
  13. must(env.KSF_FUNCTIONS_URL === 'https://kangshifu.fmode.cn/api/functions'
  14. && env.KSF_PARSE_APP_ID === 'FKieRbIoGt_main' && /^[A-Za-z0-9_-]{43}$/.test(env.KSF_SERVICE_TOKEN), 'BAD_SERVICE_CONFIG');
  15. const header = {
  16. version: 'ksf.credential-envelope.v1', purpose: PURPOSE, recipientHost: host, recipientDirectory: directory,
  17. publicKeySha256: sha256(recipient.export({ type: 'spki', format: 'der' })),
  18. encryptedAt: new Date(now).toISOString(), expiresAt, keyAlgorithm: 'RSA-OAEP-SHA256', contentAlgorithm: 'AES-256-GCM',
  19. };
  20. const aad = Buffer.from(JSON.stringify(header));
  21. const plaintext = Buffer.from(JSON.stringify({ purpose: PURPOSE, credentialId, expiresAt, env }));
  22. const key = crypto.randomBytes(32), iv = crypto.randomBytes(12);
  23. try {
  24. const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
  25. cipher.setAAD(aad);
  26. const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
  27. const wrapped = crypto.publicEncrypt({ key: recipient, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
  28. oaepHash: 'sha256', oaepLabel: Buffer.from(PURPOSE) }, key);
  29. return { header, aadBase64: aad.toString('base64'), wrappedKeyBase64: wrapped.toString('base64'),
  30. ivBase64: iv.toString('base64'), tagBase64: cipher.getAuthTag().toString('base64'), ciphertextBase64: ciphertext.toString('base64') };
  31. } finally { key.fill(0); plaintext.fill(0); }
  32. }