1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-20 02:03:39 +00:00

[PM-5614] introduce SecretState wrapper (#7823)

Matt provided a ton of help on getting the state interactions right. Both he 
and Justin collaborated with me to write the core of of the secret classifier.

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
This commit is contained in:
✨ Audrey ✨
2024-02-27 11:40:32 -05:00
committed by GitHub
parent 5a1f09a568
commit 36116bddda
13 changed files with 1198 additions and 290 deletions

View File

@@ -1,7 +1,4 @@
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
import { SymmetricCryptoKey } from "../../../../platform/models/domain/symmetric-crypto-key";
import { DefaultOptions, Forwarders, SecretPadding } from "./constants";
import { DefaultOptions, Forwarders } from "./constants";
import { ApiOptions, ForwarderId } from "./forwarder-options";
import { MaybeLeakedOptions, UsernameGeneratorOptions } from "./generator-options";
@@ -73,116 +70,3 @@ export function falsyDefault<T>(value: T, defaults: Partial<T>): T {
return value;
}
/** encrypts sensitive options and stores them in-place.
* @param encryptService The service used to encrypt the options.
* @param key The key used to encrypt the options.
* @param options The options to encrypt. The encrypted members are
* removed from the options and the decrypted members
* are added to the options.
*/
export async function encryptInPlace(
encryptService: EncryptService,
key: SymmetricCryptoKey,
options: ApiOptions & MaybeLeakedOptions,
) {
if (!options.token) {
return;
}
// pick the options that require encryption
const encryptOptions = (({ token, wasPlainText }) => ({ token, wasPlainText }))(options);
delete options.token;
delete options.wasPlainText;
// don't leak whether a leak was possible by padding the encrypted string.
// without this, it could be possible to determine whether the token was
// encrypted by checking the length of the encrypted string.
const toEncrypt = JSON.stringify(encryptOptions).padEnd(
SecretPadding.length,
SecretPadding.character,
);
const encrypted = await encryptService.encrypt(toEncrypt, key);
options.encryptedToken = encrypted;
}
/** decrypts sensitive options and stores them in-place.
* @param encryptService The service used to decrypt the options.
* @param key The key used to decrypt the options.
* @param options The options to decrypt. The encrypted members are
* removed from the options and the decrypted members
* are added to the options.
* @returns null if the options were decrypted successfully, otherwise
* a string describing why the options could not be decrypted.
* The return values are intended to be used for logging and debugging.
* @remarks This method does not throw if the options could not be decrypted
* because in such cases there's nothing the user can do to fix it.
*/
export async function decryptInPlace(
encryptService: EncryptService,
key: SymmetricCryptoKey,
options: ApiOptions & MaybeLeakedOptions,
) {
if (!options.encryptedToken) {
return "missing encryptedToken";
}
const decrypted = await encryptService.decryptToUtf8(options.encryptedToken, key);
delete options.encryptedToken;
// If the decrypted string is not exactly the padding length, it could be compromised
// and shouldn't be trusted.
if (decrypted.length !== SecretPadding.length) {
return "invalid length";
}
// JSON terminates with a closing brace, after which the plaintext repeats `character`
// If the closing brace is not found, then it could be compromised and shouldn't be trusted.
const jsonBreakpoint = decrypted.lastIndexOf("}") + 1;
if (jsonBreakpoint < 1) {
return "missing json object";
}
// If the padding contains invalid padding characters then the padding could be used
// as a side channel for arbitrary data.
if (decrypted.substring(jsonBreakpoint).match(SecretPadding.hasInvalidPadding)) {
return "invalid padding";
}
// remove padding and parse the JSON
const json = decrypted.substring(0, jsonBreakpoint);
const { decryptedOptions, error } = parseOptions(json);
if (error) {
return error;
}
Object.assign(options, decryptedOptions);
}
function parseOptions(json: string) {
let decryptedOptions = null;
try {
decryptedOptions = JSON.parse(json);
} catch {
return { decryptedOptions: undefined as string, error: "invalid json" };
}
// If the decrypted options contain any property that is not in the original
// options, then the object could be used as a side channel for arbitrary data.
if (Object.keys(decryptedOptions).some((key) => key !== "token" && key !== "wasPlainText")) {
return { decryptedOptions: undefined as string, error: "unknown keys" };
}
// If the decrypted properties are not the expected type, then the object could
// be compromised and shouldn't be trusted.
if (typeof decryptedOptions.token !== "string") {
return { decryptedOptions: undefined as string, error: "invalid token" };
}
if (decryptedOptions.wasPlainText !== undefined && decryptedOptions.wasPlainText !== true) {
return { decryptedOptions: undefined as string, error: "invalid wasPlainText" };
}
return { decryptedOptions, error: undefined as string };
}