1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-14 15:33:55 +00:00

add emailHashes and migrate emails to EncString

This commit is contained in:
John Harrington
2026-01-22 10:27:33 -07:00
parent 0a2fbc4cde
commit acb415340f
6 changed files with 62 additions and 19 deletions

View File

@@ -23,6 +23,7 @@ export class SendData {
deletionDate: string;
password: string;
emails: string;
emailHashes: string;
disabled: boolean;
hideEmail: boolean;
authType: AuthType;
@@ -46,6 +47,7 @@ export class SendData {
this.deletionDate = response.deletionDate;
this.password = response.password;
this.emails = response.emails;
this.emailHashes = "";
this.disabled = response.disable;
this.hideEmail = response.hideEmail;
this.authType = response.authType;

View File

@@ -31,7 +31,8 @@ export class Send extends Domain {
expirationDate: Date;
deletionDate: Date;
password: string;
emails: string;
emails: EncString;
emailHashes: string;
disabled: boolean;
hideEmail: boolean;
authType: AuthType;
@@ -51,6 +52,7 @@ export class Send extends Domain {
name: null,
notes: null,
key: null,
emails: null,
},
["id", "accessId"],
);
@@ -60,7 +62,7 @@ export class Send extends Domain {
this.maxAccessCount = obj.maxAccessCount;
this.accessCount = obj.accessCount;
this.password = obj.password;
this.emails = obj.emails;
this.emailHashes = obj.emailHashes;
this.disabled = obj.disabled;
this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null;
this.deletionDate = obj.deletionDate != null ? new Date(obj.deletionDate) : null;
@@ -92,8 +94,17 @@ export class Send extends Domain {
// model.key is a seed used to derive a key, not a SymmetricCryptoKey
model.key = await encryptService.decryptBytes(this.key, sendKeyEncryptionKey);
model.cryptoKey = await keyService.makeSendKey(model.key);
model.name =
this.name != null ? await encryptService.decryptString(this.name, model.cryptoKey) : null;
model.notes =
this.notes != null ? await encryptService.decryptString(this.notes, model.cryptoKey) : null;
await this.decryptObj<Send, SendView>(this, model, ["name", "notes"], model.cryptoKey);
if (this.emails != null) {
const decryptedEmails = await encryptService.decryptString(this.emails, model.cryptoKey);
model.emails = decryptedEmails ? decryptedEmails.split(",").map((e) => e.trim()) : [];
} else {
model.emails = [];
}
switch (this.type) {
case SendType.File:
@@ -122,6 +133,7 @@ export class Send extends Domain {
key: EncString.fromJSON(obj.key),
name: EncString.fromJSON(obj.name),
notes: EncString.fromJSON(obj.notes),
emails: EncString.fromJSON(obj.emails),
text: SendText.fromJSON(obj.text),
file: SendFile.fromJSON(obj.file),
revisionDate,

View File

@@ -18,6 +18,7 @@ export class SendRequest {
file: SendFileApi;
password: string;
emails: string;
emailHashes: string;
disabled: boolean;
hideEmail: boolean;
@@ -31,7 +32,8 @@ export class SendRequest {
this.deletionDate = send.deletionDate != null ? send.deletionDate.toISOString() : null;
this.key = send.key != null ? send.key.encryptedString : null;
this.password = send.password;
this.emails = send.emails;
this.emails = send.emails ? send.emails.encryptedString : null;
this.emailHashes = send.emailHashes;
this.disabled = send.disabled;
this.hideEmail = send.hideEmail;

View File

@@ -50,7 +50,6 @@ export class SendView implements View {
this.password = s.password;
this.hideEmail = s.hideEmail;
this.authType = s.authType;
this.emails = s.emails ? s.emails.split(",").map((e) => e.trim()) : [];
}
get urlB64Key(): string {

View File

@@ -148,6 +148,7 @@ export class SendApiService implements SendApiServiceAbstraction {
private async upload(sendData: [Send, EncArrayBuffer]): Promise<SendResponse> {
const request = new SendRequest(sendData[0], sendData[1]?.buffer.byteLength);
let response: SendResponse;
if (sendData[0].id == null) {
if (sendData[0].type === SendType.Text) {

View File

@@ -8,6 +8,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { PBKDF2KdfConfig, KeyService } from "@bitwarden/key-management";
import { KeyGenerationService } from "../../../key-management/crypto";
import { CryptoFunctionService } from "../../../key-management/crypto/abstractions/crypto-function.service";
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
import { EncString } from "../../../key-management/crypto/models/enc-string";
import { I18nService } from "../../../platform/abstractions/i18n.service";
@@ -51,6 +52,7 @@ export class SendService implements InternalSendServiceAbstraction {
private keyGenerationService: KeyGenerationService,
private stateProvider: SendStateProvider,
private encryptService: EncryptService,
private cryptoFunctionService: CryptoFunctionService,
) {}
async encrypt(
@@ -82,17 +84,23 @@ export class SendService implements InternalSendServiceAbstraction {
const hasEmails = (model.emails?.length ?? 0) > 0;
if (hasEmails) {
send.emails = model.emails.join(",");
const plaintextEmails = model.emails.join(",");
send.emails = await this.encryptService.encryptString(plaintextEmails, model.cryptoKey);
send.emailHashes = await this.hashEmails(plaintextEmails);
send.password = null;
} else if (password != null) {
// Note: Despite being called key, the passwordKey is not used for encryption.
// It is used as a static proof that the client knows the password, and has the encryption key.
const passwordKey = await this.keyGenerationService.deriveKeyFromPassword(
password,
model.key,
new PBKDF2KdfConfig(SEND_KDF_ITERATIONS),
);
send.password = passwordKey.keyB64;
} else {
send.emails = null;
send.emailHashes = "";
if (password != null) {
// Note: Despite being called key, the passwordKey is not used for encryption.
// It is used as a static proof that the client knows the password, and has the encryption key.
const passwordKey = await this.keyGenerationService.deriveKeyFromPassword(
password,
model.key,
new PBKDF2KdfConfig(SEND_KDF_ITERATIONS),
);
send.password = passwordKey.keyB64;
}
}
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
if (userKey == null) {
@@ -100,10 +108,14 @@ export class SendService implements InternalSendServiceAbstraction {
}
// Key is not a SymmetricCryptoKey, but key material used to derive the cryptoKey
send.key = await this.encryptService.encryptBytes(model.key, userKey);
// FIXME: model.name can be null. encryptString should not be called with null values.
send.name = await this.encryptService.encryptString(model.name, model.cryptoKey);
// FIXME: model.notes can be null. encryptString should not be called with null values.
send.notes = await this.encryptService.encryptString(model.notes, model.cryptoKey);
send.name =
model.name != null
? await this.encryptService.encryptString(model.name, model.cryptoKey)
: null;
send.notes =
model.notes != null
? await this.encryptService.encryptString(model.notes, model.cryptoKey)
: null;
if (send.type === SendType.Text) {
send.text = new SendText();
// FIXME: model.text.text can be null. encryptString should not be called with null values.
@@ -373,4 +385,19 @@ export class SendService implements InternalSendServiceAbstraction {
decryptedSends.sort(Utils.getSortFunction(this.i18nService, "name"));
return decryptedSends;
}
private async hashEmails(emails: string): Promise<string> {
if (!emails) {
return "";
}
const emailArray = emails.split(",").map((e) => e.trim().toLowerCase());
const hashPromises = emailArray.map(async (email) => {
const hash: Uint8Array = await this.cryptoFunctionService.hash(email, "sha256");
return Utils.fromBufferToHex(hash).toUpperCase();
});
const hashes = await Promise.all(hashPromises);
return hashes.join(",");
}
}