feat(server): improve crypto (#13822)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
  * Resolved cryptographic compatibility issues with OpenSSL 3.x.

* **New Features**
  * Broader support for multiple key types for signing and verification.
  * OTP generation adjusted (affects digit distribution/values).

* **Refactor**
* Internal cryptographic key handling reworked for improved stability,
compatibility, and multi-key-type support.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2025-10-29 18:12:23 +08:00
committed by GitHub
parent b7ac7caab4
commit 9904f50e0b

View File

@@ -2,13 +2,17 @@ import {
createCipheriv, createCipheriv,
createDecipheriv, createDecipheriv,
createHash, createHash,
createPrivateKey,
createPublicKey, createPublicKey,
createSign, createSign,
createVerify, createVerify,
generateKeyPairSync, generateKeyPairSync,
type KeyObject,
randomBytes, randomBytes,
randomInt, randomInt,
sign,
timingSafeEqual, timingSafeEqual,
verify,
} from 'node:crypto'; } from 'node:crypto';
import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
@@ -32,20 +36,31 @@ function generatePrivateKey(): string {
namedCurve: 'prime256v1', 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({ const key = privateKey.export({
type: 'sec1', type: 'pkcs8',
format: 'pem', format: 'pem',
}); });
return key.toString('utf8'); return key.toString('utf8');
} }
function generatePublicKey(privateKey: string) { function parseKey(privateKey: string) {
return createPublicKey({ const keyBuf = Buffer.from(privateKey);
key: Buffer.from(privateKey), let priv: KeyObject;
}) try {
.export({ format: 'pem', type: 'spki' }) priv = createPrivateKey({ key: keyBuf, format: 'pem', type: 'pkcs8' });
.toString('utf8'); } 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() @Injectable()
@@ -53,8 +68,8 @@ export class CryptoHelper implements OnModuleInit {
logger = new Logger(CryptoHelper.name); logger = new Logger(CryptoHelper.name);
keyPair!: { keyPair!: {
publicKey: Buffer; publicKey: KeyObject;
privateKey: Buffer; privateKey: KeyObject;
sha256: { sha256: {
publicKey: Buffer; publicKey: Buffer;
privateKey: Buffer; privateKey: Buffer;
@@ -87,11 +102,14 @@ export class CryptoHelper implements OnModuleInit {
private setup() { private setup() {
const privateKey = this.config.crypto.privateKey || generatePrivateKey(); 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 = { this.keyPair = {
publicKey: Buffer.from(publicKey), publicKey: pub,
privateKey: Buffer.from(privateKey), privateKey: priv,
sha256: { sha256: {
publicKey: this.sha256(publicKey), publicKey: this.sha256(publicKey),
privateKey: this.sha256(privateKey), privateKey: this.sha256(privateKey),
@@ -99,22 +117,42 @@ export class CryptoHelper implements OnModuleInit {
}; };
} }
private get keyType() {
return (this.keyPair.privateKey.asymmetricKeyType as string) || 'ec';
}
sign(data: string) { sign(data: string) {
const sign = createSign('rsa-sha256'); const input = Buffer.from(data, 'utf-8');
sign.update(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(); sign.end();
return `${data},${sign.sign(this.keyPair.privateKey, 'base64')}`; return `${data},${sign.sign(this.keyPair.privateKey, 'base64')}`;
} }
}
verify(signatureWithData: string) { verify(signatureWithData: string) {
const [data, signature] = signatureWithData.split(','); const [data, signature] = signatureWithData.split(',');
if (!signature) { if (!signature) {
return false; return false;
} }
const verify = createVerify('rsa-sha256'); const input = Buffer.from(data, 'utf-8');
verify.update(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(); verify.end();
return verify.verify(this.keyPair.privateKey, signature, 'base64'); return verify.verify(this.keyPair.publicKey, sigBuf);
}
} }
encrypt(data: string) { encrypt(data: string) {
@@ -179,7 +217,7 @@ export class CryptoHelper implements OnModuleInit {
let otp = ''; let otp = '';
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
otp += this.randomInt(0, 9).toString(); otp += this.randomInt(0, 10).toString();
} }
return otp; return otp;