Replace webcrypto RSA with PureCrypto RSA (#17742)

This commit is contained in:
Bernd Schoolmann
2025-12-03 13:11:03 +01:00
committed by GitHub
parent cf416388d7
commit a6100d8a0e
6 changed files with 48 additions and 101 deletions

View File

@@ -91,7 +91,7 @@ export abstract class CryptoFunctionService {
abstract rsaEncrypt(
data: Uint8Array,
publicKey: Uint8Array,
algorithm: "sha1" | "sha256",
algorithm: "sha1",
): Promise<Uint8Array>;
/**
* @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<Uint8Array>;
abstract rsaExtractPublicKey(privateKey: Uint8Array): Promise<Uint8Array>;
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
*/

View File

@@ -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");
}
}

View File

@@ -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 () => {

View File

@@ -263,33 +263,19 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
async rsaEncrypt(
data: Uint8Array,
publicKey: Uint8Array,
algorithm: "sha1" | "sha256",
_algorithm: "sha1",
): Promise<Uint8Array> {
// 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<Uint8Array> {
// 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<UnsignedPublicKey> {
@@ -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<CsprngArray> {
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<CsprngArray> {
const arr = new Uint8Array(length);
this.crypto.getRandomValues(arr);

View File

@@ -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<void> {
// 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 () => {

View File

@@ -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<Uint8Array> {
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<Uint8Array> {
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<UnsignedPublicKey> {
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<CsprngArray> {