diff --git a/packages/backend/server/src/base/helpers/crypto.ts b/packages/backend/server/src/base/helpers/crypto.ts index 8bb271964b..d30c98446d 100644 --- a/packages/backend/server/src/base/helpers/crypto.ts +++ b/packages/backend/server/src/base/helpers/crypto.ts @@ -2,13 +2,17 @@ import { createCipheriv, createDecipheriv, createHash, + createPrivateKey, createPublicKey, createSign, createVerify, generateKeyPairSync, + type KeyObject, randomBytes, randomInt, + sign, timingSafeEqual, + verify, } from 'node:crypto'; import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; @@ -32,20 +36,31 @@ function generatePrivateKey(): string { namedCurve: 'prime256v1', }); + // Export EC private key as PKCS#8 PEM. This avoids OpenSSL 3.x decoder issues + // in Node.js 22 when later deriving the public key via createPublicKey. const key = privateKey.export({ - type: 'sec1', + type: 'pkcs8', format: 'pem', }); return key.toString('utf8'); } -function generatePublicKey(privateKey: string) { - return createPublicKey({ - key: Buffer.from(privateKey), - }) - .export({ format: 'pem', type: 'spki' }) - .toString('utf8'); +function parseKey(privateKey: string) { + const keyBuf = Buffer.from(privateKey); + let priv: KeyObject; + try { + priv = createPrivateKey({ key: keyBuf, format: 'pem', type: 'pkcs8' }); + } catch (e1) { + try { + priv = createPrivateKey({ key: keyBuf, format: 'pem', type: 'sec1' }); + } catch (e2) { + // As a last resort rely on auto-detection + priv = createPrivateKey(keyBuf); + } + } + const pub = createPublicKey(priv); + return { priv, pub }; } @Injectable() @@ -53,8 +68,8 @@ export class CryptoHelper implements OnModuleInit { logger = new Logger(CryptoHelper.name); keyPair!: { - publicKey: Buffer; - privateKey: Buffer; + publicKey: KeyObject; + privateKey: KeyObject; sha256: { publicKey: Buffer; privateKey: Buffer; @@ -87,11 +102,14 @@ export class CryptoHelper implements OnModuleInit { private setup() { const privateKey = this.config.crypto.privateKey || generatePrivateKey(); - const publicKey = generatePublicKey(privateKey); + const { priv, pub } = parseKey(privateKey); + const publicKey = pub + .export({ format: 'pem', type: 'spki' }) + .toString('utf8'); this.keyPair = { - publicKey: Buffer.from(publicKey), - privateKey: Buffer.from(privateKey), + publicKey: pub, + privateKey: priv, sha256: { publicKey: this.sha256(publicKey), privateKey: this.sha256(privateKey), @@ -99,11 +117,23 @@ export class CryptoHelper implements OnModuleInit { }; } + private get keyType() { + return (this.keyPair.privateKey.asymmetricKeyType as string) || 'ec'; + } + sign(data: string) { - const sign = createSign('rsa-sha256'); - sign.update(data, 'utf-8'); - sign.end(); - return `${data},${sign.sign(this.keyPair.privateKey, 'base64')}`; + const input = Buffer.from(data, 'utf-8'); + if (this.keyType === 'ed25519') { + // Ed25519 signs the message directly (no pre-hash) + const sig = sign(null, input, this.keyPair.privateKey); + return `${data},${sig.toString('base64')}`; + } else { + // ECDSA with SHA-256 for EC keys + const sign = createSign('sha256'); + sign.update(input); + sign.end(); + return `${data},${sign.sign(this.keyPair.privateKey, 'base64')}`; + } } verify(signatureWithData: string) { @@ -111,10 +141,18 @@ export class CryptoHelper implements OnModuleInit { if (!signature) { return false; } - const verify = createVerify('rsa-sha256'); - verify.update(data, 'utf-8'); - verify.end(); - return verify.verify(this.keyPair.privateKey, signature, 'base64'); + const input = Buffer.from(data, 'utf-8'); + const sigBuf = Buffer.from(signature, 'base64'); + if (this.keyType === 'ed25519') { + // Ed25519 verifies the message directly + return verify(null, input, this.keyPair.publicKey, sigBuf); + } else { + // ECDSA with SHA-256 + const verify = createVerify('sha256'); + verify.update(input); + verify.end(); + return verify.verify(this.keyPair.publicKey, sigBuf); + } } encrypt(data: string) { @@ -179,7 +217,7 @@ export class CryptoHelper implements OnModuleInit { let otp = ''; for (let i = 0; i < length; i++) { - otp += this.randomInt(0, 9).toString(); + otp += this.randomInt(0, 10).toString(); } return otp;