From a6100d8a0ebe67fbb453049627879fa54b7c45db Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 3 Dec 2025 13:11:03 +0100 Subject: [PATCH] Replace webcrypto RSA with PureCrypto RSA (#17742) --- .../abstractions/crypto-function.service.ts | 6 +- .../encrypt.service.implementation.ts | 8 +-- .../web-crypto-function.service.spec.ts | 3 +- .../services/web-crypto-function.service.ts | 47 ++++--------- .../node-crypto-function.service.spec.ts | 15 +++- .../services/node-crypto-function.service.ts | 70 +++++-------------- 6 files changed, 48 insertions(+), 101 deletions(-) diff --git a/libs/common/src/key-management/crypto/abstractions/crypto-function.service.ts b/libs/common/src/key-management/crypto/abstractions/crypto-function.service.ts index 705a1c1a24..b16371198b 100644 --- a/libs/common/src/key-management/crypto/abstractions/crypto-function.service.ts +++ b/libs/common/src/key-management/crypto/abstractions/crypto-function.service.ts @@ -91,7 +91,7 @@ export abstract class CryptoFunctionService { abstract rsaEncrypt( data: Uint8Array, publicKey: Uint8Array, - algorithm: "sha1" | "sha256", + algorithm: "sha1", ): Promise; /** * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations @@ -100,10 +100,10 @@ export abstract class CryptoFunctionService { abstract rsaDecrypt( data: Uint8Array, privateKey: Uint8Array, - algorithm: "sha1" | "sha256", + algorithm: "sha1", ): Promise; abstract rsaExtractPublicKey(privateKey: Uint8Array): Promise; - abstract rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[Uint8Array, Uint8Array]>; + abstract rsaGenerateKeyPair(length: 2048): Promise<[Uint8Array, Uint8Array]>; /** * Generates a key of the given length suitable for use in AES encryption */ diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts index 132bbc306c..a5da0c8238 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts @@ -252,15 +252,9 @@ export class EncryptServiceImplementation implements EncryptService { throw new Error("[Encrypt service] rsaDecrypt: No data provided for decryption."); } - let algorithm: "sha1" | "sha256"; switch (data.encryptionType) { case EncryptionType.Rsa2048_OaepSha1_B64: case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: - algorithm = "sha1"; - break; - case EncryptionType.Rsa2048_OaepSha256_B64: - case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: - algorithm = "sha256"; break; default: throw new Error("Invalid encryption type."); @@ -270,6 +264,6 @@ export class EncryptServiceImplementation implements EncryptService { throw new Error("[Encrypt service] rsaDecrypt: No private key provided for decryption."); } - return this.cryptoFunctionService.rsaDecrypt(data.dataBytes, privateKey, algorithm); + return this.cryptoFunctionService.rsaDecrypt(data.dataBytes, privateKey, "sha1"); } } diff --git a/libs/common/src/key-management/crypto/services/web-crypto-function.service.spec.ts b/libs/common/src/key-management/crypto/services/web-crypto-function.service.spec.ts index af23a515de..c64926e0e5 100644 --- a/libs/common/src/key-management/crypto/services/web-crypto-function.service.spec.ts +++ b/libs/common/src/key-management/crypto/services/web-crypto-function.service.spec.ts @@ -299,7 +299,6 @@ describe("WebCrypto Function Service", () => { }); describe("rsaGenerateKeyPair", () => { - testRsaGenerateKeyPair(1024); testRsaGenerateKeyPair(2048); // Generating 4096 bit keys can be slow. Commenting it out to save CI. @@ -495,7 +494,7 @@ function testHmac(algorithm: "sha1" | "sha256" | "sha512", mac: string) { }); } -function testRsaGenerateKeyPair(length: 1024 | 2048 | 4096) { +function testRsaGenerateKeyPair(length: 2048) { it( "should successfully generate a " + length + " bit key pair", async () => { diff --git a/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts b/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts index 829227cada..ee0b5cab90 100644 --- a/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts +++ b/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts @@ -263,33 +263,19 @@ export class WebCryptoFunctionService implements CryptoFunctionService { async rsaEncrypt( data: Uint8Array, publicKey: Uint8Array, - algorithm: "sha1" | "sha256", + _algorithm: "sha1", ): Promise { - // Note: Edge browser requires that we specify name and hash for both key import and decrypt. - // We cannot use the proper types here. - const rsaParams = { - name: "RSA-OAEP", - hash: { name: this.toWebCryptoAlgorithm(algorithm) }, - }; - const impKey = await this.subtle.importKey("spki", publicKey, rsaParams, false, ["encrypt"]); - const buffer = await this.subtle.encrypt(rsaParams, impKey, data); - return new Uint8Array(buffer); + await SdkLoadService.Ready; + return PureCrypto.rsa_encrypt_data(data, publicKey); } async rsaDecrypt( data: Uint8Array, privateKey: Uint8Array, - algorithm: "sha1" | "sha256", + _algorithm: "sha1", ): Promise { - // Note: Edge browser requires that we specify name and hash for both key import and decrypt. - // We cannot use the proper types here. - const rsaParams = { - name: "RSA-OAEP", - hash: { name: this.toWebCryptoAlgorithm(algorithm) }, - }; - const impKey = await this.subtle.importKey("pkcs8", privateKey, rsaParams, false, ["decrypt"]); - const buffer = await this.subtle.decrypt(rsaParams, impKey, data); - return new Uint8Array(buffer); + await SdkLoadService.Ready; + return PureCrypto.rsa_decrypt_data(data, privateKey); } async rsaExtractPublicKey(privateKey: Uint8Array): Promise { @@ -297,6 +283,13 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return PureCrypto.rsa_extract_public_key(privateKey) as UnsignedPublicKey; } + async rsaGenerateKeyPair(_length: 2048): Promise<[UnsignedPublicKey, Uint8Array]> { + await SdkLoadService.Ready; + const privateKey = PureCrypto.rsa_generate_keypair(); + const publicKey = await this.rsaExtractPublicKey(privateKey); + return [publicKey, privateKey]; + } + async aesGenerateKey(bitLength = 128 | 192 | 256 | 512): Promise { if (bitLength === 512) { // 512 bit keys are not supported in WebCrypto, so we concat two 256 bit keys @@ -314,20 +307,6 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return new Uint8Array(rawKey) as CsprngArray; } - async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[Uint8Array, Uint8Array]> { - const rsaParams = { - name: "RSA-OAEP", - modulusLength: length, - publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537 - // Have to specify some algorithm - hash: { name: this.toWebCryptoAlgorithm("sha1") }, - }; - const keyPair = await this.subtle.generateKey(rsaParams, true, ["encrypt", "decrypt"]); - const publicKey = await this.subtle.exportKey("spki", keyPair.publicKey); - const privateKey = await this.subtle.exportKey("pkcs8", keyPair.privateKey); - return [new Uint8Array(publicKey), new Uint8Array(privateKey)]; - } - randomBytes(length: number): Promise { const arr = new Uint8Array(length); this.crypto.getRandomValues(arr); diff --git a/libs/node/src/services/node-crypto-function.service.spec.ts b/libs/node/src/services/node-crypto-function.service.spec.ts index 3256d85110..28a6c127d4 100644 --- a/libs/node/src/services/node-crypto-function.service.spec.ts +++ b/libs/node/src/services/node-crypto-function.service.spec.ts @@ -1,9 +1,17 @@ +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EcbDecryptParameters } from "@bitwarden/common/platform/models/domain/decrypt-parameters"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { NodeCryptoFunctionService } from "./node-crypto-function.service"; +class TestSdkLoadService extends SdkLoadService { + protected override load(): Promise { + // Simulate successful WASM load + return Promise.resolve(); + } +} + const RsaPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl0Vawl/toXzkEvB82FEtqHP" + "4xlU2ab/v0crqIfXfIoWF/XXdHGIdrZeilnRXPPJT1B9dTsasttEZNnua/0Rek/cjNDHtzT52irfoZYS7X6HNIfOi54Q+egP" + @@ -37,6 +45,10 @@ const Sha512Mac = "5ea7817a0b7c5d4d9b00364ccd214669131fc17fe4aca"; describe("NodeCrypto Function Service", () => { + beforeAll(async () => { + await new TestSdkLoadService().loadAndInit(); + }); + describe("pbkdf2", () => { const regular256Key = "pj9prw/OHPleXI6bRdmlaD+saJS4awrMiQsQiDjeu2I="; const utf8256Key = "yqvoFXgMRmHR3QPYr5pyR4uVuoHkltv9aHUP63p8n7I="; @@ -279,7 +291,6 @@ describe("NodeCrypto Function Service", () => { }); describe("rsaGenerateKeyPair", () => { - testRsaGenerateKeyPair(1024); testRsaGenerateKeyPair(2048); // Generating 4096 bit keys is really slow with Forge lib. @@ -514,7 +525,7 @@ function testCompare(fast = false) { }); } -function testRsaGenerateKeyPair(length: 1024 | 2048 | 4096) { +function testRsaGenerateKeyPair(length: 2048) { it( "should successfully generate a " + length + " bit key pair", async () => { diff --git a/libs/node/src/services/node-crypto-function.service.ts b/libs/node/src/services/node-crypto-function.service.ts index 22cc5756f3..49dbc65ca8 100644 --- a/libs/node/src/services/node-crypto-function.service.ts +++ b/libs/node/src/services/node-crypto-function.service.ts @@ -4,6 +4,7 @@ import * as forge from "node-forge"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { UnsignedPublicKey } from "@bitwarden/common/key-management/types"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { EncryptionType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { @@ -12,6 +13,7 @@ import { } from "@bitwarden/common/platform/models/domain/decrypt-parameters"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { PureCrypto } from "@bitwarden/sdk-internal"; export class NodeCryptoFunctionService implements CryptoFunctionService { pbkdf2( @@ -205,72 +207,34 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { return Promise.resolve(this.toUint8Buffer(decBuf)); } - rsaEncrypt( + async rsaEncrypt( data: Uint8Array, publicKey: Uint8Array, - algorithm: "sha1" | "sha256", + _algorithm: "sha1", ): Promise { - if (algorithm === "sha256") { - throw new Error("Node crypto does not support RSA-OAEP SHA-256"); - } - - const pem = this.toPemPublicKey(publicKey); - const decipher = crypto.publicEncrypt(pem, this.toNodeBuffer(data)); - return Promise.resolve(this.toUint8Buffer(decipher)); + await SdkLoadService.Ready; + return PureCrypto.rsa_encrypt_data(data, publicKey); } - rsaDecrypt( + async rsaDecrypt( data: Uint8Array, privateKey: Uint8Array, - algorithm: "sha1" | "sha256", + _algorithm: "sha1", ): Promise { - if (algorithm === "sha256") { - throw new Error("Node crypto does not support RSA-OAEP SHA-256"); - } - - const pem = this.toPemPrivateKey(privateKey); - const decipher = crypto.privateDecrypt(pem, this.toNodeBuffer(data)); - return Promise.resolve(this.toUint8Buffer(decipher)); + await SdkLoadService.Ready; + return PureCrypto.rsa_decrypt_data(data, privateKey); } async rsaExtractPublicKey(privateKey: Uint8Array): Promise { - const privateKeyByteString = Utils.fromBufferToByteString(privateKey); - const privateKeyAsn1 = forge.asn1.fromDer(privateKeyByteString); - const forgePrivateKey: any = forge.pki.privateKeyFromAsn1(privateKeyAsn1); - const forgePublicKey = (forge.pki as any).setRsaPublicKey(forgePrivateKey.n, forgePrivateKey.e); - const publicKeyAsn1 = forge.pki.publicKeyToAsn1(forgePublicKey); - const publicKeyByteString = forge.asn1.toDer(publicKeyAsn1).data; - const publicKeyArray = Utils.fromByteStringToArray(publicKeyByteString); - return publicKeyArray as UnsignedPublicKey; + await SdkLoadService.Ready; + return PureCrypto.rsa_extract_public_key(privateKey) as UnsignedPublicKey; } - async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[UnsignedPublicKey, Uint8Array]> { - return new Promise<[UnsignedPublicKey, Uint8Array]>((resolve, reject) => { - forge.pki.rsa.generateKeyPair( - { - bits: length, - workers: -1, - e: 0x10001, // 65537 - }, - (error, keyPair) => { - if (error != null) { - reject(error); - return; - } - - const publicKeyAsn1 = forge.pki.publicKeyToAsn1(keyPair.publicKey); - const publicKeyByteString = forge.asn1.toDer(publicKeyAsn1).getBytes(); - const publicKey = Utils.fromByteStringToArray(publicKeyByteString); - - const privateKeyAsn1 = forge.pki.privateKeyToAsn1(keyPair.privateKey); - const privateKeyPkcs8 = forge.pki.wrapRsaPrivateKey(privateKeyAsn1); - const privateKeyByteString = forge.asn1.toDer(privateKeyPkcs8).getBytes(); - const privateKey = Utils.fromByteStringToArray(privateKeyByteString); - - resolve([publicKey as UnsignedPublicKey, privateKey]); - }, - ); - }); + async rsaGenerateKeyPair(_length: 2048): Promise<[UnsignedPublicKey, Uint8Array]> { + await SdkLoadService.Ready; + const privateKey = PureCrypto.rsa_generate_keypair(); + const publicKey = await this.rsaExtractPublicKey(privateKey); + return [publicKey, privateKey]; } aesGenerateKey(bitLength: 128 | 192 | 256 | 512): Promise {