1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 05:43:41 +00:00

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( abstract rsaEncrypt(
data: Uint8Array, data: Uint8Array,
publicKey: Uint8Array, publicKey: Uint8Array,
algorithm: "sha1" | "sha256", algorithm: "sha1",
): Promise<Uint8Array>; ): Promise<Uint8Array>;
/** /**
* @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations * @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( abstract rsaDecrypt(
data: Uint8Array, data: Uint8Array,
privateKey: Uint8Array, privateKey: Uint8Array,
algorithm: "sha1" | "sha256", algorithm: "sha1",
): Promise<Uint8Array>; ): Promise<Uint8Array>;
abstract rsaExtractPublicKey(privateKey: Uint8Array): 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 * 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."); throw new Error("[Encrypt service] rsaDecrypt: No data provided for decryption.");
} }
let algorithm: "sha1" | "sha256";
switch (data.encryptionType) { switch (data.encryptionType) {
case EncryptionType.Rsa2048_OaepSha1_B64: case EncryptionType.Rsa2048_OaepSha1_B64:
case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64:
algorithm = "sha1";
break;
case EncryptionType.Rsa2048_OaepSha256_B64:
case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64:
algorithm = "sha256";
break; break;
default: default:
throw new Error("Invalid encryption type."); 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."); 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", () => { describe("rsaGenerateKeyPair", () => {
testRsaGenerateKeyPair(1024);
testRsaGenerateKeyPair(2048); testRsaGenerateKeyPair(2048);
// Generating 4096 bit keys can be slow. Commenting it out to save CI. // 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( it(
"should successfully generate a " + length + " bit key pair", "should successfully generate a " + length + " bit key pair",
async () => { async () => {

View File

@@ -263,33 +263,19 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
async rsaEncrypt( async rsaEncrypt(
data: Uint8Array, data: Uint8Array,
publicKey: Uint8Array, publicKey: Uint8Array,
algorithm: "sha1" | "sha256", _algorithm: "sha1",
): Promise<Uint8Array> { ): Promise<Uint8Array> {
// Note: Edge browser requires that we specify name and hash for both key import and decrypt. await SdkLoadService.Ready;
// We cannot use the proper types here. return PureCrypto.rsa_encrypt_data(data, publicKey);
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);
} }
async rsaDecrypt( async rsaDecrypt(
data: Uint8Array, data: Uint8Array,
privateKey: Uint8Array, privateKey: Uint8Array,
algorithm: "sha1" | "sha256", _algorithm: "sha1",
): Promise<Uint8Array> { ): Promise<Uint8Array> {
// Note: Edge browser requires that we specify name and hash for both key import and decrypt. await SdkLoadService.Ready;
// We cannot use the proper types here. return PureCrypto.rsa_decrypt_data(data, privateKey);
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);
} }
async rsaExtractPublicKey(privateKey: Uint8Array): Promise<UnsignedPublicKey> { async rsaExtractPublicKey(privateKey: Uint8Array): Promise<UnsignedPublicKey> {
@@ -297,6 +283,13 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
return PureCrypto.rsa_extract_public_key(privateKey) as UnsignedPublicKey; 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> { async aesGenerateKey(bitLength = 128 | 192 | 256 | 512): Promise<CsprngArray> {
if (bitLength === 512) { if (bitLength === 512) {
// 512 bit keys are not supported in WebCrypto, so we concat two 256 bit keys // 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; 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> { randomBytes(length: number): Promise<CsprngArray> {
const arr = new Uint8Array(length); const arr = new Uint8Array(length);
this.crypto.getRandomValues(arr); 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 { Utils } from "@bitwarden/common/platform/misc/utils";
import { EcbDecryptParameters } from "@bitwarden/common/platform/models/domain/decrypt-parameters"; import { EcbDecryptParameters } from "@bitwarden/common/platform/models/domain/decrypt-parameters";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { NodeCryptoFunctionService } from "./node-crypto-function.service"; 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 = const RsaPublicKey =
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl0Vawl/toXzkEvB82FEtqHP" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl0Vawl/toXzkEvB82FEtqHP" +
"4xlU2ab/v0crqIfXfIoWF/XXdHGIdrZeilnRXPPJT1B9dTsasttEZNnua/0Rek/cjNDHtzT52irfoZYS7X6HNIfOi54Q+egP" + "4xlU2ab/v0crqIfXfIoWF/XXdHGIdrZeilnRXPPJT1B9dTsasttEZNnua/0Rek/cjNDHtzT52irfoZYS7X6HNIfOi54Q+egP" +
@@ -37,6 +45,10 @@ const Sha512Mac =
"5ea7817a0b7c5d4d9b00364ccd214669131fc17fe4aca"; "5ea7817a0b7c5d4d9b00364ccd214669131fc17fe4aca";
describe("NodeCrypto Function Service", () => { describe("NodeCrypto Function Service", () => {
beforeAll(async () => {
await new TestSdkLoadService().loadAndInit();
});
describe("pbkdf2", () => { describe("pbkdf2", () => {
const regular256Key = "pj9prw/OHPleXI6bRdmlaD+saJS4awrMiQsQiDjeu2I="; const regular256Key = "pj9prw/OHPleXI6bRdmlaD+saJS4awrMiQsQiDjeu2I=";
const utf8256Key = "yqvoFXgMRmHR3QPYr5pyR4uVuoHkltv9aHUP63p8n7I="; const utf8256Key = "yqvoFXgMRmHR3QPYr5pyR4uVuoHkltv9aHUP63p8n7I=";
@@ -279,7 +291,6 @@ describe("NodeCrypto Function Service", () => {
}); });
describe("rsaGenerateKeyPair", () => { describe("rsaGenerateKeyPair", () => {
testRsaGenerateKeyPair(1024);
testRsaGenerateKeyPair(2048); testRsaGenerateKeyPair(2048);
// Generating 4096 bit keys is really slow with Forge lib. // 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( it(
"should successfully generate a " + length + " bit key pair", "should successfully generate a " + length + " bit key pair",
async () => { 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 { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { UnsignedPublicKey } from "@bitwarden/common/key-management/types"; 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 { EncryptionType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { import {
@@ -12,6 +13,7 @@ import {
} from "@bitwarden/common/platform/models/domain/decrypt-parameters"; } from "@bitwarden/common/platform/models/domain/decrypt-parameters";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "@bitwarden/common/types/csprng"; import { CsprngArray } from "@bitwarden/common/types/csprng";
import { PureCrypto } from "@bitwarden/sdk-internal";
export class NodeCryptoFunctionService implements CryptoFunctionService { export class NodeCryptoFunctionService implements CryptoFunctionService {
pbkdf2( pbkdf2(
@@ -205,72 +207,34 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
return Promise.resolve(this.toUint8Buffer(decBuf)); return Promise.resolve(this.toUint8Buffer(decBuf));
} }
rsaEncrypt( async rsaEncrypt(
data: Uint8Array, data: Uint8Array,
publicKey: Uint8Array, publicKey: Uint8Array,
algorithm: "sha1" | "sha256", _algorithm: "sha1",
): Promise<Uint8Array> { ): Promise<Uint8Array> {
if (algorithm === "sha256") { await SdkLoadService.Ready;
throw new Error("Node crypto does not support RSA-OAEP SHA-256"); return PureCrypto.rsa_encrypt_data(data, publicKey);
}
const pem = this.toPemPublicKey(publicKey);
const decipher = crypto.publicEncrypt(pem, this.toNodeBuffer(data));
return Promise.resolve(this.toUint8Buffer(decipher));
} }
rsaDecrypt( async rsaDecrypt(
data: Uint8Array, data: Uint8Array,
privateKey: Uint8Array, privateKey: Uint8Array,
algorithm: "sha1" | "sha256", _algorithm: "sha1",
): Promise<Uint8Array> { ): Promise<Uint8Array> {
if (algorithm === "sha256") { await SdkLoadService.Ready;
throw new Error("Node crypto does not support RSA-OAEP SHA-256"); return PureCrypto.rsa_decrypt_data(data, privateKey);
}
const pem = this.toPemPrivateKey(privateKey);
const decipher = crypto.privateDecrypt(pem, this.toNodeBuffer(data));
return Promise.resolve(this.toUint8Buffer(decipher));
} }
async rsaExtractPublicKey(privateKey: Uint8Array): Promise<UnsignedPublicKey> { async rsaExtractPublicKey(privateKey: Uint8Array): Promise<UnsignedPublicKey> {
const privateKeyByteString = Utils.fromBufferToByteString(privateKey); await SdkLoadService.Ready;
const privateKeyAsn1 = forge.asn1.fromDer(privateKeyByteString); return PureCrypto.rsa_extract_public_key(privateKey) as UnsignedPublicKey;
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;
} }
async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[UnsignedPublicKey, Uint8Array]> { async rsaGenerateKeyPair(_length: 2048): Promise<[UnsignedPublicKey, Uint8Array]> {
return new Promise<[UnsignedPublicKey, Uint8Array]>((resolve, reject) => { await SdkLoadService.Ready;
forge.pki.rsa.generateKeyPair( const privateKey = PureCrypto.rsa_generate_keypair();
{ const publicKey = await this.rsaExtractPublicKey(privateKey);
bits: length, return [publicKey, privateKey];
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]);
},
);
});
} }
aesGenerateKey(bitLength: 128 | 192 | 256 | 512): Promise<CsprngArray> { aesGenerateKey(bitLength: 128 | 192 | 256 | 512): Promise<CsprngArray> {