1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-24 00:23:17 +00:00
This commit is contained in:
Bernd Schoolmann
2025-02-24 19:59:22 +01:00
parent 754ebcaef9
commit 8ee5fb2cbc
34 changed files with 187 additions and 139 deletions

View File

@@ -158,7 +158,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
deviceKeyEncryptedDevicePrivateKey,
] = await Promise.all([
// Encrypt user key with the DevicePublicKey
this.encryptService.rsaEncrypt(userKey.key, devicePublicKey),
this.encryptService.rsaEncrypt(userKey.toEncoded(), devicePublicKey),
// Encrypt devicePublicKey with user key
this.encryptService.encrypt(devicePublicKey, userKey),
@@ -226,7 +226,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
// Encrypt the brand new user key with the now-decrypted public key for the device
const encryptedNewUserKey = await this.encryptService.rsaEncrypt(
newUserKey.key,
newUserKey.toEncoded(),
decryptedDevicePublicKey,
);

View File

@@ -95,7 +95,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id;
const organization = await this.getManagingOrganization(userId);
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
const keyConnectorRequest = new KeyConnectorUserKeyRequest(Utils.fromBufferToB64(masterKey.getInnerKey().encryptionKey));
try {
await this.apiService.postUserKeyToKeyConnector(
@@ -153,11 +153,11 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
: new Argon2KdfConfig(kdfIterations, kdfMemory, kdfParallelism);
const masterKey = await this.keyService.makeMasterKey(
password.keyB64,
password.toBase64(),
await this.tokenService.getEmail(),
kdfConfig,
);
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
const keyConnectorRequest = new KeyConnectorUserKeyRequest(Utils.fromBufferToB64(masterKey.getInnerKey().encryptionKey));
await this.masterPasswordService.setMasterKey(masterKey, userId);
const userKey = await this.keyService.makeUserKey(masterKey);

View File

@@ -2,6 +2,9 @@
// @ts-strict-ignore
import { firstValueFrom, map, Observable } from "rxjs";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CryptoClient } from "@bitwarden/sdk-internal";
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
import { LogService } from "../../../platform/abstractions/log.service";
@@ -191,6 +194,16 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
newKey,
"Content: User Key; Encrypting Key: Stretched Master Key",
);
if (decUserKey.length === 32 || decUserKey.length === 64) {
return new SymmetricCryptoKey(decUserKey) as UserKey;
} else {
this.logService.info("[MasterPasswordService] Userkey in new format; using sdk");
const decodedKey = CryptoClient.decode_userkey(decUserKey);
if (decodedKey.Aes256CbcHmac != null) {
const key = Utils.fromB64ToArray(decodedKey.Aes256CbcHmac);
return new SymmetricCryptoKey(key) as UserKey;
}
}
} else {
throw new Error("Unsupported encryption type.");
}

View File

@@ -53,7 +53,7 @@ export class PasswordResetEnrollmentServiceImplementation
userId ?? (await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))));
userKey = userKey ?? (await this.keyService.getUserKey(userId));
// RSA Encrypt user's userKey.key with organization public key
const encryptedKey = await this.encryptService.rsaEncrypt(userKey.key, orgPublicKey);
const encryptedKey = await this.encryptService.rsaEncrypt(userKey.toEncoded(), orgPublicKey);
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
resetRequest.resetPasswordKey = encryptedKey.encryptedString;

View File

@@ -100,7 +100,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE,
[FeatureFlag.SSHKeyVaultItem]: FALSE,
[FeatureFlag.SSHAgent]: FALSE,
[FeatureFlag.UserKeyRotationV2]: FALSE,
[FeatureFlag.UserKeyRotationV2]: true,
[FeatureFlag.CipherKeyEncryption]: FALSE,
[FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader]: FALSE,
[FeatureFlag.TrialPaymentOptional]: FALSE,

View File

@@ -36,7 +36,6 @@ export abstract class EncryptService {
): Promise<Uint8Array>;
abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString>;
abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise<Uint8Array>;
abstract resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey;
/**
* @deprecated Replaced by BulkEncryptService, remove once the feature is tested and the featureflag PM-4154-multi-worker-encryption-service is removed
* @param items The items to decrypt

View File

@@ -44,7 +44,7 @@ export class EncryptServiceImplementation implements EncryptService {
const iv = Utils.fromBufferToB64(encObj.iv);
const data = Utils.fromBufferToB64(encObj.data);
const mac = encObj.mac != null ? Utils.fromBufferToB64(encObj.mac) : null;
return new EncString(encObj.key.encType, data, iv, mac);
return new EncString(encObj.key.encryptionType(), data, iv, mac);
}
async encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer> {
@@ -59,7 +59,7 @@ export class EncryptServiceImplementation implements EncryptService {
}
const encBytes = new Uint8Array(1 + encValue.iv.byteLength + macLen + encValue.data.byteLength);
encBytes.set([encValue.key.encType]);
encBytes.set([encValue.key.encryptionType()]);
encBytes.set(new Uint8Array(encValue.iv), 1);
if (encValue.mac != null) {
encBytes.set(new Uint8Array(encValue.mac), 1 + encValue.iv.byteLength);
@@ -78,13 +78,11 @@ export class EncryptServiceImplementation implements EncryptService {
throw new Error("No key provided for decryption.");
}
key = this.resolveLegacyKey(key, encString);
// DO NOT REMOVE OR MOVE. This prevents downgrade to mac-less CBC, which would compromise integrity and confidentiality.
if (key.macKey != null && encString?.mac == null) {
this.logService.error(
"[Encrypt service] Key has mac key but payload is missing mac bytes. Key type " +
encryptionTypeName(key.encType) +
encryptionTypeName(key.encryptionType()) +
"Payload type " +
encryptionTypeName(encString.encryptionType),
"Decrypt context: " + decryptContext,
@@ -92,10 +90,10 @@ export class EncryptServiceImplementation implements EncryptService {
return null;
}
if (key.encType !== encString.encryptionType) {
if (key.encryptionType() !== encString.encryptionType) {
this.logService.error(
"[Encrypt service] Key encryption type does not match payload encryption type. Key type " +
encryptionTypeName(key.encType) +
encryptionTypeName(key.encryptionType()) +
"Payload type " +
encryptionTypeName(encString.encryptionType),
"Decrypt context: " + decryptContext,
@@ -119,7 +117,7 @@ export class EncryptServiceImplementation implements EncryptService {
if (!macsEqual) {
this.logMacFailed(
"[Encrypt service] decryptToUtf8 MAC comparison failed. Key or payload has changed. Key type " +
encryptionTypeName(key.encType) +
encryptionTypeName(key.encryptionType()) +
"Payload type " +
encryptionTypeName(encString.encryptionType) +
" Decrypt context: " +
@@ -145,13 +143,11 @@ export class EncryptServiceImplementation implements EncryptService {
throw new Error("Nothing provided for decryption.");
}
key = this.resolveLegacyKey(key, encThing);
// DO NOT REMOVE OR MOVE. This prevents downgrade to mac-less CBC, which would compromise integrity and confidentiality.
if (key.macKey != null && encThing.macBytes == null) {
this.logService.error(
"[Encrypt service] Key has mac key but payload is missing mac bytes. Key type " +
encryptionTypeName(key.encType) +
encryptionTypeName(key.encryptionType()) +
" Payload type " +
encryptionTypeName(encThing.encryptionType) +
" Decrypt context: " +
@@ -160,10 +156,10 @@ export class EncryptServiceImplementation implements EncryptService {
return null;
}
if (key.encType !== encThing.encryptionType) {
if (key.encryptionType() !== encThing.encryptionType) {
this.logService.error(
"[Encrypt service] Key encryption type does not match payload encryption type. Key type " +
encryptionTypeName(key.encType) +
encryptionTypeName(key.encryptionType()) +
" Payload type " +
encryptionTypeName(encThing.encryptionType) +
" Decrypt context: " +
@@ -181,7 +177,7 @@ export class EncryptServiceImplementation implements EncryptService {
this.logMacFailed(
"[Encrypt service#decryptToBytes] Failed to compute MAC." +
" Key type " +
encryptionTypeName(key.encType) +
encryptionTypeName(key.encryptionType()) +
" Payload type " +
encryptionTypeName(encThing.encryptionType) +
" Decrypt context: " +
@@ -195,7 +191,7 @@ export class EncryptServiceImplementation implements EncryptService {
this.logMacFailed(
"[Encrypt service#decryptToBytes]: MAC comparison failed. Key or payload has changed." +
" Key type " +
encryptionTypeName(key.encType) +
encryptionTypeName(key.encryptionType()) +
" Payload type " +
encryptionTypeName(encThing.encryptionType) +
" Decrypt context: " +
@@ -208,7 +204,7 @@ export class EncryptServiceImplementation implements EncryptService {
const result = await this.cryptoFunctionService.aesDecrypt(
encThing.dataBytes,
encThing.ivBytes,
key.encKey,
key.encryptionType()
"cbc",
);
@@ -278,19 +274,23 @@ export class EncryptServiceImplementation implements EncryptService {
}
private async aesEncrypt(data: Uint8Array, key: SymmetricCryptoKey): Promise<EncryptedObject> {
const obj = new EncryptedObject();
obj.key = key;
obj.iv = await this.cryptoFunctionService.randomBytes(16);
obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, obj.key.encKey);
const innerKey = key.getInnerKey();
if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
const obj = new EncryptedObject();
// AES-CBC Encrypt the data
obj.iv = await this.cryptoFunctionService.randomBytes(16);
obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, innerKey.encryptionKey);
if (obj.key.macKey != null) {
// Calculate the HMAC
const macData = new Uint8Array(obj.iv.byteLength + obj.data.byteLength);
macData.set(new Uint8Array(obj.iv), 0);
macData.set(new Uint8Array(obj.data), obj.iv.byteLength);
obj.mac = await this.cryptoFunctionService.hmac(macData, obj.key.macKey, "sha256");
}
obj.mac = await this.cryptoFunctionService.hmac(macData, innerKey.authenticationKey, "sha256");
return obj;
return obj;
} else {
throw new Error("Unsupported encryption type.");
}
}
private logMacFailed(msg: string) {
@@ -298,19 +298,4 @@ export class EncryptServiceImplementation implements EncryptService {
this.logService.error(msg);
}
}
/**
* Transform into new key for the old encrypt-then-mac scheme if required, otherwise return the current key unchanged
* @param encThing The encrypted object (e.g. encString or encArrayBuffer) that you want to decrypt
*/
resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey {
if (
encThing.encryptionType === EncryptionType.AesCbc128_HmacSha256_B64 &&
key.encType === EncryptionType.AesCbc256_B64
) {
return new SymmetricCryptoKey(key.key, EncryptionType.AesCbc128_HmacSha256_B64);
}
return key;
}
}

View File

@@ -1,10 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
export class EncryptedObject {
iv: Uint8Array;
data: Uint8Array;
mac: Uint8Array;
key: SymmetricCryptoKey;
}

View File

@@ -2,64 +2,61 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { CryptoClient, ExportedUserKey } from "@bitwarden/sdk-internal";
import { Utils } from "../../../platform/misc/utils";
import { EncryptionType } from "../../enums";
export class SymmetricCryptoKey {
key: Uint8Array;
encKey: Uint8Array;
macKey?: Uint8Array;
encType: EncryptionType;
type Aes256CbcHmacKey = {
type: EncryptionType.AesCbc256_HmacSha256_B64;
encryptionKey: Uint8Array;
authenticationKey: Uint8Array;
};
keyB64: string;
encKeyB64: string;
macKeyB64: string;
type Aes256CbcKey = {
type: EncryptionType.AesCbc256_B64;
encryptionKey: Uint8Array;
};
export class SymmetricCryptoKey {
private key: Aes256CbcKey | Aes256CbcHmacKey;
private newFormat = false;
meta: any;
constructor(key: Uint8Array, encType?: EncryptionType) {
constructor(key: Uint8Array, new_format = false) {
if (key == null) {
throw new Error("Must provide key");
}
if (encType == null) {
if (key.byteLength === 32) {
encType = EncryptionType.AesCbc256_B64;
} else if (key.byteLength === 64) {
encType = EncryptionType.AesCbc256_HmacSha256_B64;
} else {
throw new Error("Unable to determine encType.");
if (key.byteLength === 32) {
this.key = {
type: EncryptionType.AesCbc256_B64,
encryptionKey: key,
}
} else if (key.byteLength === 64) {
this.key = {
type: EncryptionType.AesCbc256_HmacSha256_B64,
encryptionKey: key.slice(0, 32),
authenticationKey: key.slice(32),
}
} else if (key.byteLength > 64) {
const decoded_key = CryptoClient.decode_userkey(key).Aes256CbcHmac;
this.key = {
type: EncryptionType.AesCbc256_HmacSha256_B64,
encryptionKey: decoded_key.encryption_key,
authenticationKey: decoded_key.authentication_key,
}
}
this.key = key;
this.encType = encType;
if (encType === EncryptionType.AesCbc256_B64 && key.byteLength === 32) {
this.encKey = key;
this.macKey = null;
} else if (encType === EncryptionType.AesCbc128_HmacSha256_B64 && key.byteLength === 32) {
this.encKey = key.slice(0, 16);
this.macKey = key.slice(16, 32);
} else if (encType === EncryptionType.AesCbc256_HmacSha256_B64 && key.byteLength === 64) {
this.encKey = key.slice(0, 32);
this.macKey = key.slice(32, 64);
} else {
throw new Error("Unsupported encType/key length.");
}
this.keyB64 = Utils.fromBufferToB64(this.key);
this.encKeyB64 = Utils.fromBufferToB64(this.encKey);
if (this.macKey != null) {
this.macKeyB64 = Utils.fromBufferToB64(this.macKey);
throw new Error("Unable to determine encType.");
}
}
toJSON() {
// The whole object is constructed from the initial key, so just store the B64 key
return { keyB64: this.keyB64 };
getInnerKey(): Aes256CbcKey | Aes256CbcHmacKey {
return this.key;
}
static fromString(s: string): SymmetricCryptoKey {
if (s == null) {
return null;
@@ -69,7 +66,51 @@ export class SymmetricCryptoKey {
return new SymmetricCryptoKey(arrayBuffer);
}
// For test only
toJSON() {
// The whole object is constructed from the initial key, so just store the B64 key
return { keyB64: Utils.fromBufferToB64(this) };
}
// For test only
static fromJSON(obj: Jsonify<SymmetricCryptoKey>): SymmetricCryptoKey {
return SymmetricCryptoKey.fromString(obj?.keyB64);
}
toSdkKey(): ExportedUserKey {
if (this.key.type === EncryptionType.AesCbc256_B64) {
throw new Error("Unsupported encryption type.");
} else if (this.key.type === EncryptionType.AesCbc256_HmacSha256_B64) {
return {
Aes256CbcHmac: {
encryption_key: this.key.encryptionKey,
authentication_key: this.key.authenticationKey,
},
};
} else {
throw new Error("Unsupported encryption type.");
}
}
toBase64(): string {
return Utils.fromBufferToB64(this.toEncoded());
}
toEncoded(): Uint8Array {
if (this.newFormat) {
return CryptoClient.encode_userkey(this.toSdkKey());
} else {
if (this.key.type === EncryptionType.AesCbc256_B64) {
return this.key.encryptionKey;
} else if (this.key.type === EncryptionType.AesCbc256_HmacSha256_B64) {
return new Uint8Array([...this.key.encryptionKey, ...this.key.authenticationKey]);
} else {
throw new Error("Unsupported encryption type.");
}
}
}
encryptionType(): EncryptionType {
return this.key.type;
}
}

View File

@@ -80,8 +80,8 @@ export class KeyGenerationService implements KeyGenerationServiceAbstraction {
async stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey> {
const newKey = new Uint8Array(64);
const encKey = await this.cryptoFunctionService.hkdfExpand(key.key, "enc", 32, "sha256");
const macKey = await this.cryptoFunctionService.hkdfExpand(key.key, "mac", 32, "sha256");
const encKey = await this.cryptoFunctionService.hkdfExpand(key.getInnerKey().encryptionKey, "enc", 32, "sha256");
const macKey = await this.cryptoFunctionService.hkdfExpand(key.getInnerKey().encryptionKey, "mac", 32, "sha256");
newKey.set(new Uint8Array(encKey));
newKey.set(new Uint8Array(macKey), 32);

View File

@@ -136,7 +136,7 @@ export class DefaultSdkService implements SdkService {
) {
await client.crypto().initialize_user_crypto({
email: account.email,
method: { decryptedKey: { decrypted_user_key: userKey.keyB64 } },
method: { decryptedKey: { decrypted_user_key: userKey.toBase64() } },
kdfParams:
kdfParams.kdfType === KdfType.PBKDF2_SHA256
? {

View File

@@ -251,7 +251,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
}
if (p.encKey == null) {
p.encKey = forge.util.decode64(key.encKeyB64);
p.encKey = forge.util.decode64(Utils.fromBufferToB64(key.getInnerKey().encryptionKey));
}
p.data = forge.util.decode64(data);
p.iv = forge.util.decode64(iv);

View File

@@ -76,7 +76,7 @@ export class SendService implements InternalSendServiceAbstraction {
model.key,
new PBKDF2KdfConfig(SEND_KDF_ITERATIONS),
);
send.password = passwordKey.keyB64;
send.password = passwordKey.toBase64();
}
if (key == null) {
key = await this.keyService.getUserKey();

View File

@@ -279,7 +279,7 @@ export class CipherService implements CipherServiceAbstraction {
key,
).then(async () => {
if (model.key != null) {
attachment.key = await this.encryptService.encrypt(model.key.key, key);
attachment.key = await this.encryptService.encrypt(model.key.toEncoded(), key);
}
encAttachments.push(attachment);
});
@@ -1770,7 +1770,7 @@ export class CipherService implements CipherServiceAbstraction {
// Then, we have to encrypt the cipher key with the proper key.
cipher.key = await this.encryptService.encrypt(
decryptedCipherKey.key,
decryptedCipherKey.toEncoded(),
keyForCipherKeyEncryption,
);