[PM-3732] Use subtle to make aes keys (#6162)
* Provide `aesGenerateKey` to make aes keys * Use aesGenerateKey when generating a key data * Fix device test
This commit is contained in:
parent
615248e04f
commit
0448910806
|
@ -53,7 +53,7 @@ export class AccessService {
|
|||
serviceAccountId: string,
|
||||
accessTokenView: AccessTokenView
|
||||
): Promise<string> {
|
||||
const keyMaterial = await this.cryptoFunctionService.randomBytes(16);
|
||||
const keyMaterial = await this.cryptoFunctionService.aesGenerateKey(128);
|
||||
const key = await this.cryptoFunctionService.hkdf(
|
||||
keyMaterial,
|
||||
"bitwarden-accesstoken",
|
||||
|
|
|
@ -167,7 +167,7 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac
|
|||
|
||||
private async makeDeviceKey(): Promise<DeviceKey> {
|
||||
// Create 512-bit device key
|
||||
const randomBytes: CsprngArray = await this.cryptoFunctionService.randomBytes(64);
|
||||
const randomBytes: CsprngArray = await this.cryptoFunctionService.aesGenerateKey(512);
|
||||
const deviceKey = new SymmetricCryptoKey(randomBytes) as DeviceKey;
|
||||
|
||||
return deviceKey;
|
||||
|
|
|
@ -168,16 +168,16 @@ describe("deviceTrustCryptoService", () => {
|
|||
it("creates a new non-null 64 byte device key, securely stores it, and returns it", async () => {
|
||||
const mockRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
|
||||
|
||||
const cryptoFuncSvcRandomBytesSpy = jest
|
||||
.spyOn(cryptoFunctionService, "randomBytes")
|
||||
const cryptoFuncSvcGenerateKeySpy = jest
|
||||
.spyOn(cryptoFunctionService, "aesGenerateKey")
|
||||
.mockResolvedValue(mockRandomBytes);
|
||||
|
||||
// TypeScript will allow calling private methods if the object is of type 'any'
|
||||
// This is a hacky workaround, but it allows for cleaner tests
|
||||
const deviceKey = await (deviceTrustCryptoService as any).makeDeviceKey();
|
||||
|
||||
expect(cryptoFuncSvcRandomBytesSpy).toHaveBeenCalledTimes(1);
|
||||
expect(cryptoFuncSvcRandomBytesSpy).toHaveBeenCalledWith(deviceKeyBytesLength);
|
||||
expect(cryptoFuncSvcGenerateKeySpy).toHaveBeenCalledTimes(1);
|
||||
expect(cryptoFuncSvcGenerateKeySpy).toHaveBeenCalledWith(deviceKeyBytesLength * 8);
|
||||
|
||||
expect(deviceKey).not.toBeNull();
|
||||
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
|
||||
|
|
|
@ -93,7 +93,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
|||
keyConnectorUrl: legacyKeyConnectorUrl,
|
||||
userDecryptionOptions,
|
||||
} = tokenResponse;
|
||||
const password = await this.cryptoFunctionService.randomBytes(64);
|
||||
const password = await this.cryptoFunctionService.aesGenerateKey(512);
|
||||
const kdfConfig = new KdfConfig(kdfIterations, kdfMemory, kdfParallelism);
|
||||
|
||||
const masterKey = await this.cryptoService.makeMasterKey(
|
||||
|
|
|
@ -66,5 +66,14 @@ export abstract class CryptoFunctionService {
|
|||
) => Promise<Uint8Array>;
|
||||
rsaExtractPublicKey: (privateKey: Uint8Array) => Promise<Uint8Array>;
|
||||
rsaGenerateKeyPair: (length: 1024 | 2048 | 4096) => Promise<[Uint8Array, Uint8Array]>;
|
||||
/**
|
||||
* Generates a key of the given length suitable for use in AES encryption
|
||||
*/
|
||||
aesGenerateKey: (bitLength: 128 | 192 | 256 | 512) => Promise<CsprngArray>;
|
||||
/**
|
||||
* Generates a random array of bytes of the given length. Uses a cryptographically secure random number generator.
|
||||
*
|
||||
* Do not use this for generating encryption keys. Use aesGenerateKey or rsaGenerateKeyPair instead.
|
||||
*/
|
||||
randomBytes: (length: number) => Promise<CsprngArray>;
|
||||
}
|
||||
|
|
|
@ -119,7 +119,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||
throw new Error("No Master Key found.");
|
||||
}
|
||||
|
||||
const newUserKey = await this.cryptoFunctionService.randomBytes(64);
|
||||
const newUserKey = await this.cryptoFunctionService.aesGenerateKey(512);
|
||||
return this.buildProtectedSymmetricKey(masterKey, newUserKey);
|
||||
}
|
||||
|
||||
|
@ -367,7 +367,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||
throw new Error("No key provided");
|
||||
}
|
||||
|
||||
const newSymKey = await this.cryptoFunctionService.randomBytes(64);
|
||||
const newSymKey = await this.cryptoFunctionService.aesGenerateKey(512);
|
||||
return this.buildProtectedSymmetricKey(key, newSymKey);
|
||||
}
|
||||
|
||||
|
@ -458,7 +458,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||
}
|
||||
|
||||
async makeOrgKey<T extends OrgKey | ProviderKey>(): Promise<[EncString, T]> {
|
||||
const shareKey = await this.cryptoFunctionService.randomBytes(64);
|
||||
const shareKey = await this.cryptoFunctionService.aesGenerateKey(512);
|
||||
const publicKey = await this.getPublicKey();
|
||||
const encShareKey = await this.rsaEncrypt(shareKey, publicKey);
|
||||
return [encShareKey, new SymmetricCryptoKey(shareKey) as T];
|
||||
|
@ -731,8 +731,8 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||
publicKey: string;
|
||||
privateKey: EncString;
|
||||
}> {
|
||||
const randomBytes = await this.cryptoFunctionService.randomBytes(64);
|
||||
const userKey = new SymmetricCryptoKey(randomBytes) as UserKey;
|
||||
const rawKey = await this.cryptoFunctionService.aesGenerateKey(512);
|
||||
const userKey = new SymmetricCryptoKey(rawKey) as UserKey;
|
||||
const [publicKey, privateKey] = await this.makeKeyPair(userKey);
|
||||
await this.setUserKey(userKey);
|
||||
await this.stateService.setEncryptedPrivateKey(privateKey.encryptedString);
|
||||
|
|
|
@ -354,6 +354,20 @@ describe("WebCrypto Function Service", () => {
|
|||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("aesGenerateKey", () => {
|
||||
it.each([128, 192, 256, 512])("Should make a key of %s bits long", async (length) => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const key = await cryptoFunctionService.aesGenerateKey(length);
|
||||
expect(key.byteLength * 8).toBe(length);
|
||||
});
|
||||
|
||||
it("should not repeat itself for 512 length special case", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const key = await cryptoFunctionService.aesGenerateKey(512);
|
||||
expect(key.slice(0, 32)).not.toEqual(key.slice(32, 64));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function testPbkdf2(
|
||||
|
|
|
@ -347,6 +347,23 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
|||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
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
|
||||
const key1 = await this.aesGenerateKey(256);
|
||||
const key2 = await this.aesGenerateKey(256);
|
||||
return new Uint8Array([...key1, ...key2]) as CsprngArray;
|
||||
}
|
||||
const aesParams = {
|
||||
name: "AES-CBC",
|
||||
length: bitLength,
|
||||
};
|
||||
|
||||
const key = await this.subtle.generateKey(aesParams, true, ["encrypt", "decrypt"]);
|
||||
const rawKey = await this.subtle.exportKey("raw", key);
|
||||
return new Uint8Array(rawKey) as CsprngArray;
|
||||
}
|
||||
|
||||
async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[Uint8Array, Uint8Array]> {
|
||||
const rsaParams = {
|
||||
name: "RSA-OAEP",
|
||||
|
@ -355,10 +372,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
|||
// Have to specify some algorithm
|
||||
hash: { name: this.toWebCryptoAlgorithm("sha1") },
|
||||
};
|
||||
const keyPair = (await this.subtle.generateKey(rsaParams, true, [
|
||||
"encrypt",
|
||||
"decrypt",
|
||||
])) as CryptoKeyPair;
|
||||
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)];
|
||||
|
|
|
@ -70,7 +70,7 @@ export class SendService implements InternalSendServiceAbstraction {
|
|||
send.hideEmail = model.hideEmail;
|
||||
send.maxAccessCount = model.maxAccessCount;
|
||||
if (model.key == null) {
|
||||
model.key = await this.cryptoFunctionService.randomBytes(16);
|
||||
model.key = await this.cryptoFunctionService.aesGenerateKey(128);
|
||||
model.cryptoKey = await this.cryptoService.makeSendKey(model.key);
|
||||
}
|
||||
if (password != null) {
|
||||
|
|
|
@ -271,6 +271,15 @@ describe("NodeCrypto Function Service", () => {
|
|||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("aesGenerateKey", () => {
|
||||
it("should delegate to randomBytes", async () => {
|
||||
const nodeCryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const spy = jest.spyOn(nodeCryptoFunctionService, "randomBytes");
|
||||
await nodeCryptoFunctionService.aesGenerateKey(256);
|
||||
expect(spy).toHaveBeenCalledWith(32);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function testPbkdf2(
|
||||
|
|
|
@ -271,6 +271,10 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
|
|||
});
|
||||
}
|
||||
|
||||
aesGenerateKey(bitLength: 128 | 192 | 256 | 512): Promise<CsprngArray> {
|
||||
return this.randomBytes(bitLength / 8);
|
||||
}
|
||||
|
||||
randomBytes(length: number): Promise<CsprngArray> {
|
||||
return new Promise<CsprngArray>((resolve, reject) => {
|
||||
crypto.randomBytes(length, (error, bytes) => {
|
||||
|
|
Loading…
Reference in New Issue