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
@@ -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;