mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 21:50:15 +00:00
Merge branch 'km/refactor-symmetric-keys-1' into km/cose
This commit is contained in:
@@ -300,7 +300,7 @@ describe("MainBiometricsService", function () {
|
||||
|
||||
expect(userKey).not.toBeNull();
|
||||
expect(userKey!.keyB64).toBe(biometricKey);
|
||||
expect(userKey!.encType).toBe(EncryptionType.AesCbc256_HmacSha256_B64);
|
||||
expect(userKey!.inner().type).toBe(EncryptionType.AesCbc256_HmacSha256_B64);
|
||||
expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith(
|
||||
"Bitwarden_biometric",
|
||||
`${userId}_user_biometric`,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { biometrics, passwords } from "@bitwarden/desktop-napi";
|
||||
@@ -218,7 +220,13 @@ export default class OsBiometricsServiceWindows implements OsBiometricService {
|
||||
symmetricKey: SymmetricCryptoKey,
|
||||
clientKeyPartB64: string | undefined,
|
||||
): biometrics.KeyMaterial {
|
||||
const key = symmetricKey?.macKeyB64 ?? symmetricKey?.keyB64;
|
||||
let key = null;
|
||||
const innerKey = symmetricKey.inner();
|
||||
if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
|
||||
key = Utils.fromBufferToB64(innerKey.authenticationKey);
|
||||
} else {
|
||||
key = Utils.fromBufferToB64(innerKey.encryptionKey);
|
||||
}
|
||||
|
||||
const result = {
|
||||
osKeyPartB64: key,
|
||||
|
||||
@@ -103,7 +103,9 @@ describe("AuthRequestService", () => {
|
||||
});
|
||||
|
||||
it("should use the master key and hash if they exist", async () => {
|
||||
masterPasswordService.masterKeySubject.next({ encKey: new Uint8Array(64) } as MasterKey);
|
||||
masterPasswordService.masterKeySubject.next(
|
||||
new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey,
|
||||
);
|
||||
masterPasswordService.masterKeyHashSubject.next("MASTER_KEY_HASH");
|
||||
|
||||
await sut.approveOrDenyAuthRequest(
|
||||
@@ -111,7 +113,7 @@ describe("AuthRequestService", () => {
|
||||
new AuthRequestResponse({ id: "123", publicKey: "KEY" }),
|
||||
);
|
||||
|
||||
expect(encryptService.rsaEncrypt).toHaveBeenCalledWith(new Uint8Array(64), expect.anything());
|
||||
expect(encryptService.rsaEncrypt).toHaveBeenCalledWith(new Uint8Array(32), expect.anything());
|
||||
});
|
||||
|
||||
it("should use the user key if the master key and hash do not exist", async () => {
|
||||
|
||||
@@ -14,7 +14,10 @@ import { AuthRequestPushNotification } from "@bitwarden/common/models/response/n
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import {
|
||||
Aes256CbcKey,
|
||||
SymmetricCryptoKey,
|
||||
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import {
|
||||
AUTH_REQUEST_DISK_LOCAL,
|
||||
StateProvider,
|
||||
@@ -111,7 +114,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
|
||||
Utils.fromUtf8ToArray(masterKeyHash),
|
||||
pubKey,
|
||||
);
|
||||
keyToEncrypt = masterKey.encKey;
|
||||
keyToEncrypt = (masterKey.inner() as Aes256CbcKey).encryptionKey;
|
||||
} else {
|
||||
const userKey = await this.keyService.getUserKey();
|
||||
keyToEncrypt = userKey.key;
|
||||
|
||||
@@ -252,7 +252,9 @@ describe("KeyConnectorService", () => {
|
||||
const organization = organizationData(true, true, "https://key-connector-url.com", 2, false);
|
||||
const masterKey = getMockMasterKey();
|
||||
masterPasswordService.masterKeySubject.next(masterKey);
|
||||
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
|
||||
const keyConnectorRequest = new KeyConnectorUserKeyRequest(
|
||||
Utils.fromBufferToB64(masterKey.inner().encryptionKey),
|
||||
);
|
||||
|
||||
jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization);
|
||||
jest.spyOn(apiService, "postUserKeyToKeyConnector").mockResolvedValue();
|
||||
@@ -273,7 +275,9 @@ describe("KeyConnectorService", () => {
|
||||
// Arrange
|
||||
const organization = organizationData(true, true, "https://key-connector-url.com", 2, false);
|
||||
const masterKey = getMockMasterKey();
|
||||
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
|
||||
const keyConnectorRequest = new KeyConnectorUserKeyRequest(
|
||||
Utils.fromBufferToB64(masterKey.inner().encryptionKey),
|
||||
);
|
||||
const error = new Error("Failed to post user key to key connector");
|
||||
organizationService.organizations$.mockReturnValue(of([organization]));
|
||||
|
||||
|
||||
@@ -95,7 +95,9 @@ 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.inner().encryptionKey),
|
||||
);
|
||||
|
||||
try {
|
||||
await this.apiService.postUserKeyToKeyConnector(
|
||||
@@ -157,7 +159,9 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
||||
await this.tokenService.getEmail(),
|
||||
kdfConfig,
|
||||
);
|
||||
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
|
||||
const keyConnectorRequest = new KeyConnectorUserKeyRequest(
|
||||
Utils.fromBufferToB64(masterKey.inner().encryptionKey),
|
||||
);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||
|
||||
const userKey = await this.keyService.makeUserKey(masterKey);
|
||||
|
||||
@@ -13,7 +13,11 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { EncryptedObject } from "@bitwarden/common/platform/models/domain/encrypted-object";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import {
|
||||
Aes256CbcHmacKey,
|
||||
Aes256CbcKey,
|
||||
SymmetricCryptoKey,
|
||||
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { PureCrypto } from "@bitwarden/sdk-internal";
|
||||
|
||||
import {
|
||||
@@ -55,11 +59,16 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
plainBuf = plainValue;
|
||||
}
|
||||
|
||||
const encObj = await this.aesEncrypt(plainBuf, key);
|
||||
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);
|
||||
const innerKey = key.inner();
|
||||
if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
|
||||
const encObj = await this.aesEncrypt(plainBuf, innerKey);
|
||||
const iv = Utils.fromBufferToB64(encObj.iv);
|
||||
const data = Utils.fromBufferToB64(encObj.data);
|
||||
const mac = Utils.fromBufferToB64(encObj.mac);
|
||||
return new EncString(innerKey.type, data, iv, mac);
|
||||
} else {
|
||||
throw new Error(`Encrypt is not supported for keys of type ${innerKey.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
async encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer> {
|
||||
@@ -67,8 +76,26 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
throw new Error("No encryption key provided.");
|
||||
}
|
||||
|
||||
const encValue = await this.aesEncrypt(plainValue, key);
|
||||
return EncArrayBuffer.fromParts(encValue.key.encType, encValue.iv, encValue.data, encValue.mac);
|
||||
const innerKey = key.inner();
|
||||
if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
|
||||
const encValue = await this.aesEncrypt(plainValue, innerKey);
|
||||
const macLen = encValue.mac.length;
|
||||
const encBytes = new Uint8Array(
|
||||
1 + encValue.iv.byteLength + macLen + encValue.data.byteLength,
|
||||
);
|
||||
encBytes.set([innerKey.type]);
|
||||
encBytes.set(new Uint8Array(encValue.iv), 1);
|
||||
encBytes.set(new Uint8Array(encValue.mac), 1 + encValue.iv.byteLength);
|
||||
encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength + macLen);
|
||||
return new EncArrayBuffer(encBytes);
|
||||
} else if (innerKey.type === EncryptionType.AesCbc256_B64) {
|
||||
const encValue = await this.aesEncryptLegacy(plainValue, innerKey);
|
||||
const encBytes = new Uint8Array(1 + encValue.iv.byteLength + encValue.data.byteLength);
|
||||
encBytes.set([innerKey.type]);
|
||||
encBytes.set(new Uint8Array(encValue.iv), 1);
|
||||
encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength);
|
||||
return new EncArrayBuffer(encBytes);
|
||||
}
|
||||
}
|
||||
|
||||
async decryptToUtf8(
|
||||
@@ -89,36 +116,25 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
throw new Error("No key provided for decryption.");
|
||||
}
|
||||
|
||||
// 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) +
|
||||
"Payload type " +
|
||||
encryptionTypeName(encString.encryptionType),
|
||||
"Decrypt context: " + decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const innerKey = key.inner();
|
||||
if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
|
||||
if (encString.encryptionType !== EncryptionType.AesCbc256_HmacSha256_B64) {
|
||||
this.logDecryptError(
|
||||
"Key encryption type does not match payload encryption type",
|
||||
key.inner().type,
|
||||
encString.encryptionType,
|
||||
decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key.encType !== encString.encryptionType) {
|
||||
this.logService.error(
|
||||
"[Encrypt service] Key encryption type does not match payload encryption type. Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
"Payload type " +
|
||||
encryptionTypeName(encString.encryptionType),
|
||||
"Decrypt context: " + decryptContext,
|
||||
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(
|
||||
encString.data,
|
||||
encString.iv,
|
||||
encString.mac,
|
||||
key,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(
|
||||
encString.data,
|
||||
encString.iv,
|
||||
encString.mac,
|
||||
key,
|
||||
);
|
||||
if (fastParams.macKey != null && fastParams.mac != null) {
|
||||
const computedMac = await this.cryptoFunctionService.hmacFast(
|
||||
fastParams.macData,
|
||||
fastParams.macKey,
|
||||
@@ -127,18 +143,41 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac);
|
||||
if (!macsEqual) {
|
||||
this.logMacFailed(
|
||||
"[Encrypt service] decryptToUtf8 MAC comparison failed. Key or payload has changed. Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
"Payload type " +
|
||||
encryptionTypeName(encString.encryptionType) +
|
||||
" Decrypt context: " +
|
||||
decryptContext,
|
||||
"decryptToUtf8 MAC comparison failed. Key or payload has changed.",
|
||||
key.inner().type,
|
||||
encString.encryptionType,
|
||||
decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return await this.cryptoFunctionService.aesDecryptFast({
|
||||
mode: "cbc",
|
||||
parameters: fastParams,
|
||||
});
|
||||
} else if (innerKey.type === EncryptionType.AesCbc256_B64) {
|
||||
if (encString.encryptionType !== EncryptionType.AesCbc256_B64) {
|
||||
this.logDecryptError(
|
||||
"Key encryption type does not match payload encryption type",
|
||||
key.inner().type,
|
||||
encString.encryptionType,
|
||||
decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return await this.cryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters: fastParams });
|
||||
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(
|
||||
encString.data,
|
||||
encString.iv,
|
||||
null,
|
||||
key,
|
||||
);
|
||||
return await this.cryptoFunctionService.aesDecryptFast({
|
||||
mode: "cbc",
|
||||
parameters: fastParams,
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Unsupported encryption type`);
|
||||
}
|
||||
}
|
||||
|
||||
async decryptToBytes(
|
||||
@@ -173,72 +212,64 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
throw new Error("Nothing provided for decryption.");
|
||||
}
|
||||
|
||||
// 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) +
|
||||
" Payload type " +
|
||||
encryptionTypeName(encThing.encryptionType) +
|
||||
" Decrypt context: " +
|
||||
const inner = key.inner();
|
||||
if (inner.type === EncryptionType.AesCbc256_HmacSha256_B64) {
|
||||
if (
|
||||
encThing.encryptionType !== EncryptionType.AesCbc256_HmacSha256_B64 ||
|
||||
encThing.macBytes === null
|
||||
) {
|
||||
this.logDecryptError(
|
||||
"Encryption key type mismatch",
|
||||
inner.type,
|
||||
encThing.encryptionType,
|
||||
decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key.encType !== encThing.encryptionType) {
|
||||
this.logService.error(
|
||||
"[Encrypt service] Key encryption type does not match payload encryption type. Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
" Payload type " +
|
||||
encryptionTypeName(encThing.encryptionType) +
|
||||
" Decrypt context: " +
|
||||
decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key.macKey != null && encThing.macBytes != null) {
|
||||
const macData = new Uint8Array(encThing.ivBytes.byteLength + encThing.dataBytes.byteLength);
|
||||
macData.set(new Uint8Array(encThing.ivBytes), 0);
|
||||
macData.set(new Uint8Array(encThing.dataBytes), encThing.ivBytes.byteLength);
|
||||
const computedMac = await this.cryptoFunctionService.hmac(macData, key.macKey, "sha256");
|
||||
if (computedMac === null) {
|
||||
this.logMacFailed(
|
||||
"[Encrypt service#decryptToBytes] Failed to compute MAC." +
|
||||
" Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
" Payload type " +
|
||||
encryptionTypeName(encThing.encryptionType) +
|
||||
" Decrypt context: " +
|
||||
decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const computedMac = await this.cryptoFunctionService.hmac(
|
||||
macData,
|
||||
inner.authenticationKey,
|
||||
"sha256",
|
||||
);
|
||||
const macsMatch = await this.cryptoFunctionService.compare(encThing.macBytes, computedMac);
|
||||
if (!macsMatch) {
|
||||
this.logMacFailed(
|
||||
"[Encrypt service#decryptToBytes]: MAC comparison failed. Key or payload has changed." +
|
||||
" Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
" Payload type " +
|
||||
encryptionTypeName(encThing.encryptionType) +
|
||||
" Decrypt context: " +
|
||||
decryptContext,
|
||||
"MAC comparison failed. Key or payload has changed.",
|
||||
inner.type,
|
||||
encThing.encryptionType,
|
||||
decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.cryptoFunctionService.aesDecrypt(
|
||||
encThing.dataBytes,
|
||||
encThing.ivBytes,
|
||||
inner.encryptionKey,
|
||||
"cbc",
|
||||
);
|
||||
} else if (inner.type === EncryptionType.AesCbc256_B64) {
|
||||
if (encThing.encryptionType !== EncryptionType.AesCbc256_B64) {
|
||||
this.logDecryptError(
|
||||
"Encryption key type mismatch",
|
||||
inner.type,
|
||||
encThing.encryptionType,
|
||||
decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.cryptoFunctionService.aesDecrypt(
|
||||
encThing.dataBytes,
|
||||
encThing.ivBytes,
|
||||
inner.encryptionKey,
|
||||
"cbc",
|
||||
);
|
||||
}
|
||||
|
||||
const result = await this.cryptoFunctionService.aesDecrypt(
|
||||
encThing.dataBytes,
|
||||
encThing.ivBytes,
|
||||
key.encKey,
|
||||
"cbc",
|
||||
);
|
||||
|
||||
return result ?? null;
|
||||
}
|
||||
|
||||
async rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString> {
|
||||
@@ -303,25 +334,48 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
return Utils.fromBufferToB64(hashArray);
|
||||
}
|
||||
|
||||
private async aesEncrypt(data: Uint8Array, key: SymmetricCryptoKey): Promise<EncryptedObject> {
|
||||
private async aesEncrypt(data: Uint8Array, key: Aes256CbcHmacKey): 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);
|
||||
obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, key.encryptionKey);
|
||||
|
||||
if (obj.key.macKey != null) {
|
||||
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");
|
||||
}
|
||||
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, key.authenticationKey, "sha256");
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private logMacFailed(msg: string) {
|
||||
/**
|
||||
* @deprecated Removed once AesCbc256_B64 support is removed
|
||||
*/
|
||||
private async aesEncryptLegacy(data: Uint8Array, key: Aes256CbcKey): Promise<EncryptedObject> {
|
||||
const obj = new EncryptedObject();
|
||||
obj.iv = await this.cryptoFunctionService.randomBytes(16);
|
||||
obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, key.encryptionKey);
|
||||
return obj;
|
||||
}
|
||||
|
||||
private logDecryptError(
|
||||
msg: string,
|
||||
keyEncType: EncryptionType,
|
||||
dataEncType: EncryptionType,
|
||||
decryptContext: string,
|
||||
) {
|
||||
this.logService.error(
|
||||
`[Encrypt service] ${msg} Key type ${encryptionTypeName(keyEncType)} Payload type ${encryptionTypeName(dataEncType)} Decrypt context: ${decryptContext}`,
|
||||
);
|
||||
}
|
||||
|
||||
private logMacFailed(
|
||||
msg: string,
|
||||
keyEncType: EncryptionType,
|
||||
dataEncType: EncryptionType,
|
||||
decryptContext: string,
|
||||
) {
|
||||
if (this.logMacFailures) {
|
||||
this.logService.error(msg);
|
||||
this.logDecryptError(msg, keyEncType, dataEncType, decryptContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@ import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import {
|
||||
Aes256CbcHmacKey,
|
||||
SymmetricCryptoKey,
|
||||
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
|
||||
import { makeStaticByteArray } from "../../../../spec";
|
||||
@@ -150,7 +153,7 @@ describe("EncryptService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("decrypts data with provided key for Aes256Cbc", async () => {
|
||||
it("decrypts data with provided key for Aes256CbcHmac", async () => {
|
||||
const decryptedBytes = makeStaticByteArray(10, 200);
|
||||
|
||||
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(1));
|
||||
@@ -162,7 +165,7 @@ describe("EncryptService", () => {
|
||||
expect(cryptoFunctionService.aesDecrypt).toBeCalledWith(
|
||||
expect.toEqualBuffer(encBuffer.dataBytes),
|
||||
expect.toEqualBuffer(encBuffer.ivBytes),
|
||||
expect.toEqualBuffer(key.encKey),
|
||||
expect.toEqualBuffer(key.inner().encryptionKey),
|
||||
"cbc",
|
||||
);
|
||||
|
||||
@@ -183,7 +186,7 @@ describe("EncryptService", () => {
|
||||
expect(cryptoFunctionService.aesDecrypt).toBeCalledWith(
|
||||
expect.toEqualBuffer(encBuffer.dataBytes),
|
||||
expect.toEqualBuffer(encBuffer.ivBytes),
|
||||
expect.toEqualBuffer(key.encKey),
|
||||
expect.toEqualBuffer(key.inner().encryptionKey),
|
||||
"cbc",
|
||||
);
|
||||
|
||||
@@ -201,7 +204,7 @@ describe("EncryptService", () => {
|
||||
|
||||
expect(cryptoFunctionService.hmac).toBeCalledWith(
|
||||
expect.toEqualBuffer(expectedMacData),
|
||||
key.macKey,
|
||||
(key.inner() as Aes256CbcHmacKey).authenticationKey,
|
||||
"sha256",
|
||||
);
|
||||
|
||||
@@ -257,9 +260,9 @@ describe("EncryptService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("decrypts data with provided key for Aes256Cbc_HmacSha256", async () => {
|
||||
it("decrypts data with provided key for AesCbc256_HmacSha256", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
|
||||
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data", "iv", "mac");
|
||||
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data");
|
||||
cryptoFunctionService.aesDecryptFastParameters.mockReturnValue({
|
||||
macData: makeStaticByteArray(32, 0),
|
||||
macKey: makeStaticByteArray(32, 0),
|
||||
@@ -277,10 +280,14 @@ describe("EncryptService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("decrypts data with provided key for Aes256Cbc", async () => {
|
||||
it("decrypts data with provided key for AesCbc256", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0));
|
||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
|
||||
cryptoFunctionService.aesDecryptFastParameters.mockReturnValue({} as any);
|
||||
cryptoFunctionService.aesDecryptFastParameters.mockReturnValue({
|
||||
macData: makeStaticByteArray(32, 0),
|
||||
macKey: makeStaticByteArray(32, 0),
|
||||
mac: makeStaticByteArray(32, 0),
|
||||
} as any);
|
||||
cryptoFunctionService.hmacFast.mockResolvedValue(makeStaticByteArray(32, 0));
|
||||
cryptoFunctionService.compareFast.mockResolvedValue(true);
|
||||
cryptoFunctionService.aesDecryptFast.mockResolvedValue("data");
|
||||
@@ -290,7 +297,7 @@ describe("EncryptService", () => {
|
||||
expect(cryptoFunctionService.compareFast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns null if key is Aes256Cbc_HmacSha256 but EncString is Aes256Cbc", async () => {
|
||||
it("returns null if key is AesCbc256_HMAC but encstring is AesCbc256", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
|
||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
|
||||
|
||||
@@ -299,9 +306,9 @@ describe("EncryptService", () => {
|
||||
expect(logService.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns null if key is Aes256Cbc but encstring is AesCbc256_HmacSha256", async () => {
|
||||
it("returns null if key is AesCbc256 but encstring is AesCbc256_HMAC", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0));
|
||||
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data", "iv", "mac");
|
||||
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data");
|
||||
|
||||
const actual = await encryptService.decryptToUtf8(encString, key);
|
||||
expect(actual).toBeNull();
|
||||
@@ -310,7 +317,7 @@ describe("EncryptService", () => {
|
||||
|
||||
it("returns null if macs don't match", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
|
||||
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data", "iv", "mac");
|
||||
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data");
|
||||
cryptoFunctionService.aesDecryptFastParameters.mockReturnValue({
|
||||
macData: makeStaticByteArray(32, 0),
|
||||
macKey: makeStaticByteArray(32, 0),
|
||||
@@ -325,25 +332,6 @@ describe("EncryptService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("decryptToUtf8", () => {
|
||||
it("throws if no key is provided", () => {
|
||||
return expect(encryptService.decryptToUtf8(null, null)).rejects.toThrow(
|
||||
"No key provided for decryption.",
|
||||
);
|
||||
});
|
||||
it("returns null if key is mac key but encstring has no mac", async () => {
|
||||
const key = new SymmetricCryptoKey(
|
||||
makeStaticByteArray(64, 0),
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
);
|
||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
|
||||
|
||||
const actual = await encryptService.decryptToUtf8(encString, key);
|
||||
expect(actual).toBeNull();
|
||||
expect(logService.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("rsa", () => {
|
||||
const data = makeStaticByteArray(10, 100);
|
||||
const encryptedData = makeStaticByteArray(10, 150);
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { makeStaticByteArray } from "../../../../spec";
|
||||
import { EncryptionType } from "../../enums";
|
||||
import { Utils } from "../../misc/utils";
|
||||
|
||||
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
|
||||
import { Aes256CbcHmacKey, SymmetricCryptoKey } from "./symmetric-crypto-key";
|
||||
|
||||
describe("SymmetricCryptoKey", () => {
|
||||
it("errors if no key", () => {
|
||||
@@ -18,12 +19,12 @@ describe("SymmetricCryptoKey", () => {
|
||||
const cryptoKey = new SymmetricCryptoKey(key);
|
||||
|
||||
expect(cryptoKey).toEqual({
|
||||
encKey: key,
|
||||
encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
|
||||
encType: EncryptionType.AesCbc256_B64,
|
||||
key: key,
|
||||
keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
|
||||
macKey: null,
|
||||
innerKey: {
|
||||
type: EncryptionType.AesCbc256_B64,
|
||||
encryptionKey: key,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,14 +33,14 @@ describe("SymmetricCryptoKey", () => {
|
||||
const cryptoKey = new SymmetricCryptoKey(key);
|
||||
|
||||
expect(cryptoKey).toEqual({
|
||||
encKey: key.slice(0, 32),
|
||||
encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
|
||||
encType: EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
key: key,
|
||||
keyB64:
|
||||
"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==",
|
||||
macKey: key.slice(32, 64),
|
||||
macKeyB64: "ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=",
|
||||
innerKey: {
|
||||
type: EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
encryptionKey: key.slice(0, 32),
|
||||
authenticationKey: key.slice(32),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,7 +49,7 @@ describe("SymmetricCryptoKey", () => {
|
||||
new SymmetricCryptoKey(makeStaticByteArray(30));
|
||||
};
|
||||
|
||||
expect(t).toThrowError("Unable to determine encType.");
|
||||
expect(t).toThrowError(`Unsupported encType/key length 30`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,6 +70,41 @@ describe("SymmetricCryptoKey", () => {
|
||||
expect(actual).toBeInstanceOf(SymmetricCryptoKey);
|
||||
});
|
||||
|
||||
it("inner returns inner key", () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const actual = key.inner();
|
||||
|
||||
expect(actual).toEqual({
|
||||
type: EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
encryptionKey: key.inner().encryptionKey,
|
||||
authenticationKey: (key.inner() as Aes256CbcHmacKey).authenticationKey,
|
||||
});
|
||||
});
|
||||
|
||||
it("toEncoded returns encoded key for AesCbc256_B64", () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(32));
|
||||
const actual = key.toEncoded();
|
||||
|
||||
expect(actual).toEqual(key.inner().encryptionKey);
|
||||
});
|
||||
|
||||
it("toEncoded returns encoded key for AesCbc256_HmacSha256_B64", () => {
|
||||
const keyBytes = makeStaticByteArray(64);
|
||||
const key = new SymmetricCryptoKey(keyBytes);
|
||||
const actual = key.toEncoded();
|
||||
|
||||
expect(actual).toEqual(keyBytes);
|
||||
});
|
||||
|
||||
it("toBase64 returns base64 encoded key", () => {
|
||||
const keyBytes = makeStaticByteArray(64);
|
||||
const keyB64 = Utils.fromBufferToB64(keyBytes);
|
||||
const key = new SymmetricCryptoKey(keyBytes);
|
||||
const actual = key.toBase64();
|
||||
|
||||
expect(actual).toEqual(keyB64);
|
||||
});
|
||||
|
||||
describe("fromString", () => {
|
||||
it("null string returns null", () => {
|
||||
const actual = SymmetricCryptoKey.fromString(null);
|
||||
|
||||
@@ -5,50 +5,53 @@ import { Jsonify } from "type-fest";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { EncryptionType } from "../../enums";
|
||||
|
||||
export type Aes256CbcHmacKey = {
|
||||
type: EncryptionType.AesCbc256_HmacSha256_B64;
|
||||
encryptionKey: Uint8Array;
|
||||
authenticationKey: Uint8Array;
|
||||
};
|
||||
|
||||
export type Aes256CbcKey = {
|
||||
type: EncryptionType.AesCbc256_B64;
|
||||
encryptionKey: Uint8Array;
|
||||
};
|
||||
|
||||
/**
|
||||
* A symmetric crypto key represents a symmetric key usable for symmetric encryption and decryption operations.
|
||||
* The specific algorithm used is private to the key, and should only be exposed to encrypt service implementations.
|
||||
* This can be done via `inner()`.
|
||||
*/
|
||||
export class SymmetricCryptoKey {
|
||||
private innerKey: Aes256CbcHmacKey | Aes256CbcKey;
|
||||
|
||||
key: Uint8Array;
|
||||
encKey: Uint8Array;
|
||||
macKey?: Uint8Array;
|
||||
encType: EncryptionType;
|
||||
|
||||
keyB64: string;
|
||||
encKeyB64: string;
|
||||
macKeyB64: string;
|
||||
|
||||
meta: any;
|
||||
|
||||
constructor(key: Uint8Array, encType?: EncryptionType) {
|
||||
/**
|
||||
* @param key The key in one of the permitted serialization formats
|
||||
*/
|
||||
constructor(key: Uint8Array) {
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
this.key = key;
|
||||
this.encType = encType;
|
||||
|
||||
if (encType === EncryptionType.AesCbc256_B64 && key.byteLength === 32) {
|
||||
this.encKey = key;
|
||||
this.macKey = null;
|
||||
} else if (encType === EncryptionType.AesCbc256_HmacSha256_B64 && key.byteLength === 64) {
|
||||
this.encKey = key.slice(0, 32);
|
||||
this.macKey = key.slice(32, 64);
|
||||
if (key.byteLength === 32) {
|
||||
this.innerKey = {
|
||||
type: EncryptionType.AesCbc256_B64,
|
||||
encryptionKey: key,
|
||||
};
|
||||
this.key = key;
|
||||
this.keyB64 = this.toBase64();
|
||||
} else if (key.byteLength === 64) {
|
||||
this.innerKey = {
|
||||
type: EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
encryptionKey: key.slice(0, 32),
|
||||
authenticationKey: key.slice(32),
|
||||
};
|
||||
this.key = key;
|
||||
this.keyB64 = this.toBase64();
|
||||
} 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(`Unsupported encType/key length ${key.byteLength}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +60,45 @@ export class SymmetricCryptoKey {
|
||||
return { keyB64: this.keyB64 };
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The inner key instance that can be directly used for encryption primitives
|
||||
*/
|
||||
inner(): Aes256CbcHmacKey | Aes256CbcKey {
|
||||
return this.innerKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The serialized key in base64 format
|
||||
*/
|
||||
toBase64(): string {
|
||||
return Utils.fromBufferToB64(this.toEncoded());
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the key to a format that can be written to state or shared
|
||||
* The currently permitted format is:
|
||||
* - AesCbc256_B64: 32 bytes (the raw key)
|
||||
* - AesCbc256_HmacSha256_B64: 64 bytes (32 bytes encryption key, 32 bytes authentication key, concatenated)
|
||||
*
|
||||
* @returns The serialized key that can be written to state or encrypted and then written to state / shared
|
||||
*/
|
||||
toEncoded(): Uint8Array {
|
||||
if (this.innerKey.type === EncryptionType.AesCbc256_B64) {
|
||||
return this.innerKey.encryptionKey;
|
||||
} else if (this.innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
|
||||
const encodedKey = new Uint8Array(64);
|
||||
encodedKey.set(this.innerKey.encryptionKey, 0);
|
||||
encodedKey.set(this.innerKey.authenticationKey, 32);
|
||||
return encodedKey;
|
||||
} else {
|
||||
throw new Error("Unsupported encryption type.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param s The serialized key in base64 format
|
||||
* @returns A SymmetricCryptoKey instance
|
||||
*/
|
||||
static fromString(s: string): SymmetricCryptoKey {
|
||||
if (s == null) {
|
||||
return null;
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as forge from "node-forge";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
|
||||
import { EncryptionType } from "../enums";
|
||||
import { CbcDecryptParameters, EcbDecryptParameters } from "../models/domain/decrypt-parameters";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
|
||||
@@ -244,37 +245,26 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
mac: string | null,
|
||||
key: SymmetricCryptoKey,
|
||||
): CbcDecryptParameters<string> {
|
||||
const p = {} as CbcDecryptParameters<string>;
|
||||
if (key.meta != null) {
|
||||
p.encKey = key.meta.encKeyByteString;
|
||||
p.macKey = key.meta.macKeyByteString;
|
||||
const innerKey = key.inner();
|
||||
if (innerKey.type === EncryptionType.AesCbc256_B64) {
|
||||
return {
|
||||
iv: forge.util.decode64(iv),
|
||||
data: forge.util.decode64(data),
|
||||
encKey: forge.util.createBuffer(innerKey.encryptionKey).getBytes(),
|
||||
} as CbcDecryptParameters<string>;
|
||||
} else if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
|
||||
const macData = forge.util.decode64(iv) + forge.util.decode64(data);
|
||||
return {
|
||||
iv: forge.util.decode64(iv),
|
||||
data: forge.util.decode64(data),
|
||||
encKey: forge.util.createBuffer(innerKey.encryptionKey).getBytes(),
|
||||
macKey: forge.util.createBuffer(innerKey.authenticationKey).getBytes(),
|
||||
mac: forge.util.decode64(mac!),
|
||||
macData,
|
||||
} as CbcDecryptParameters<string>;
|
||||
} else {
|
||||
throw new Error("Unsupported encryption type.");
|
||||
}
|
||||
|
||||
if (p.encKey == null) {
|
||||
p.encKey = forge.util.decode64(key.encKeyB64);
|
||||
}
|
||||
p.data = forge.util.decode64(data);
|
||||
p.iv = forge.util.decode64(iv);
|
||||
p.macData = p.iv + p.data;
|
||||
if (p.macKey == null && key.macKeyB64 != null) {
|
||||
p.macKey = forge.util.decode64(key.macKeyB64);
|
||||
}
|
||||
if (mac != null) {
|
||||
p.mac = forge.util.decode64(mac);
|
||||
}
|
||||
|
||||
// cache byte string keys for later
|
||||
if (key.meta == null) {
|
||||
key.meta = {};
|
||||
}
|
||||
if (key.meta.encKeyByteString == null) {
|
||||
key.meta.encKeyByteString = p.encKey;
|
||||
}
|
||||
if (p.macKey != null && key.meta.macKeyByteString == null) {
|
||||
key.meta.macKeyByteString = p.macKey;
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
aesDecryptFast({
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as crypto from "crypto";
|
||||
import * as forge from "node-forge";
|
||||
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import {
|
||||
CbcDecryptParameters,
|
||||
@@ -172,24 +173,33 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
|
||||
mac: string | null,
|
||||
key: SymmetricCryptoKey,
|
||||
): CbcDecryptParameters<Uint8Array> {
|
||||
const p = {} as CbcDecryptParameters<Uint8Array>;
|
||||
p.encKey = key.encKey;
|
||||
p.data = Utils.fromB64ToArray(data);
|
||||
p.iv = Utils.fromB64ToArray(iv);
|
||||
const dataBytes = Utils.fromB64ToArray(data);
|
||||
const ivBytes = Utils.fromB64ToArray(iv);
|
||||
const macBytes = mac != null ? Utils.fromB64ToArray(mac) : null;
|
||||
|
||||
const macData = new Uint8Array(p.iv.byteLength + p.data.byteLength);
|
||||
macData.set(new Uint8Array(p.iv), 0);
|
||||
macData.set(new Uint8Array(p.data), p.iv.byteLength);
|
||||
p.macData = macData;
|
||||
const innerKey = key.inner();
|
||||
|
||||
if (key.macKey != null) {
|
||||
p.macKey = key.macKey;
|
||||
if (innerKey.type === EncryptionType.AesCbc256_B64) {
|
||||
return {
|
||||
iv: ivBytes,
|
||||
data: dataBytes,
|
||||
encKey: innerKey.encryptionKey,
|
||||
} as CbcDecryptParameters<Uint8Array>;
|
||||
} else if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
|
||||
const macData = new Uint8Array(ivBytes.length + dataBytes.length);
|
||||
macData.set(ivBytes, 0);
|
||||
macData.set(dataBytes, ivBytes.length);
|
||||
return {
|
||||
iv: ivBytes,
|
||||
data: dataBytes,
|
||||
mac: macBytes,
|
||||
macData: macData,
|
||||
encKey: innerKey.encryptionKey,
|
||||
macKey: innerKey.authenticationKey,
|
||||
} as CbcDecryptParameters<Uint8Array>;
|
||||
} else {
|
||||
throw new Error("Unsupported encryption type");
|
||||
}
|
||||
if (mac != null) {
|
||||
p.mac = Utils.fromB64ToArray(mac);
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
async aesDecryptFast({
|
||||
|
||||
Reference in New Issue
Block a user