Files
core2026/app/gateway-api/src/auth/passwordEnvelope.ts
T

88 lines
2.7 KiB
TypeScript

import {
createHash,
createPrivateKey,
createPublicKey,
generateKeyPairSync,
privateDecrypt,
type KeyObject,
constants,
} from 'node:crypto';
export interface PasswordEnvelopeInput {
keyId: string;
ciphertext: string;
}
export interface PasswordPublicKey {
keyId: string;
algorithm: 'RSA-OAEP-256';
publicKeyPem: string;
}
export interface PasswordEnvelopeService {
getPublicKey(): PasswordPublicKey;
open(input: PasswordEnvelopeInput): string;
}
const MAX_PASSWORD_BYTES = 128;
const buildKeyId = (publicKey: KeyObject): string =>
createHash('sha256').update(publicKey.export({ format: 'der', type: 'spki' })).digest('base64url').slice(0, 22);
const decodeUtf8 = (value: Buffer): string => {
if (value.length === 0 || value.length > MAX_PASSWORD_BYTES) {
throw new Error('Password envelope has an invalid length.');
}
const decoded = value.toString('utf8');
if (!Buffer.from(decoded, 'utf8').equals(value)) {
throw new Error('Password envelope is not valid UTF-8.');
}
return decoded;
};
export const createPasswordEnvelopeService = (privateKeyPem?: string): PasswordEnvelopeService => {
const privateKey = privateKeyPem
? createPrivateKey(privateKeyPem)
: generateKeyPairSync('rsa', {
modulusLength: 3072,
publicExponent: 0x10001,
}).privateKey;
const publicKey = createPublicKey(privateKey.export({ format: 'pem', type: 'pkcs8' }));
const keyId = buildKeyId(publicKey);
const publicKeyPem = publicKey.export({ format: 'pem', type: 'spki' }).toString();
return {
getPublicKey: () => ({
keyId,
algorithm: 'RSA-OAEP-256',
publicKeyPem,
}),
open: (input) => {
if (input.keyId !== keyId) {
throw new Error('Password encryption key has changed.');
}
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(input.ciphertext)) {
throw new Error('Password envelope is not valid base64.');
}
let ciphertext: Buffer;
try {
ciphertext = Buffer.from(input.ciphertext, 'base64');
} catch {
throw new Error('Password envelope is not valid base64.');
}
if (!ciphertext.length) {
throw new Error('Password envelope is empty.');
}
const plaintext = privateDecrypt(
{
key: privateKey,
padding: constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256',
},
ciphertext
);
return decodeUtf8(plaintext);
},
};
};