mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 16:23:44 +00:00
* encode username for uri and add spec * verify response from getHibpBreach method * test/validate for BreachAccountResponse type and length instead of mock response * - extract dirt api method out of global api service - create new directory structure - change imports accordingly - extract breach account response - put extracted code into new dirt dir * codeowners and dep injection for new hibp service
89 lines
3.2 KiB
TypeScript
89 lines
3.2 KiB
TypeScript
import { Subject } from "rxjs";
|
|
import { mergeMap } from "rxjs/operators";
|
|
|
|
import { ApiService } from "../abstractions/api.service";
|
|
import { AuditService as AuditServiceAbstraction } from "../abstractions/audit.service";
|
|
import { BreachAccountResponse } from "../dirt/models/response/breach-account.response";
|
|
import { HibpApiService } from "../dirt/services/hibp-api.service";
|
|
import { CryptoFunctionService } from "../key-management/crypto/abstractions/crypto-function.service";
|
|
import { ErrorResponse } from "../models/response/error.response";
|
|
import { Utils } from "../platform/misc/utils";
|
|
|
|
const PwnedPasswordsApi = "https://api.pwnedpasswords.com/range/";
|
|
|
|
export class AuditService implements AuditServiceAbstraction {
|
|
private passwordLeakedSubject = new Subject<{
|
|
password: string;
|
|
resolve: (count: number) => void;
|
|
reject: (err: any) => void;
|
|
}>();
|
|
|
|
constructor(
|
|
private cryptoFunctionService: CryptoFunctionService,
|
|
private apiService: ApiService,
|
|
private hibpApiService: HibpApiService,
|
|
private readonly maxConcurrent: number = 100, // default to 100, can be overridden
|
|
) {
|
|
this.maxConcurrent = maxConcurrent;
|
|
this.passwordLeakedSubject
|
|
.pipe(
|
|
mergeMap(
|
|
// Handle each password leak request, resolving or rejecting the associated promise.
|
|
async (req) => {
|
|
try {
|
|
const count = await this.fetchLeakedPasswordCount(req.password);
|
|
req.resolve(count);
|
|
} catch (err) {
|
|
req.reject(err);
|
|
}
|
|
},
|
|
this.maxConcurrent, // Limit concurrent API calls
|
|
),
|
|
)
|
|
.subscribe();
|
|
}
|
|
|
|
async passwordLeaked(password: string): Promise<number> {
|
|
return new Promise<number>((resolve, reject) => {
|
|
this.passwordLeakedSubject.next({ password, resolve, reject });
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetches the count of leaked passwords from the Pwned Passwords API.
|
|
* @param password The password to check.
|
|
* @returns A promise that resolves to the number of times the password has been leaked.
|
|
*/
|
|
protected async fetchLeakedPasswordCount(password: string): Promise<number> {
|
|
const hashBytes = await this.cryptoFunctionService.hash(password, "sha1");
|
|
const hash = Utils.fromBufferToHex(hashBytes).toUpperCase();
|
|
const hashStart = hash.substr(0, 5);
|
|
const hashEnding = hash.substr(5);
|
|
|
|
const response = await this.apiService.nativeFetch(new Request(PwnedPasswordsApi + hashStart));
|
|
const leakedHashes = await response.text();
|
|
const match = leakedHashes.split(/\r?\n/).find((v) => {
|
|
return v.split(":")[0] === hashEnding;
|
|
});
|
|
|
|
return match != null ? parseInt(match.split(":")[1], 10) : 0;
|
|
}
|
|
|
|
async breachedAccounts(username: string): Promise<BreachAccountResponse[]> {
|
|
try {
|
|
return await this.hibpApiService.getHibpBreach(username);
|
|
} catch (e) {
|
|
const error = e as ErrorResponse;
|
|
if (error.statusCode === 404) {
|
|
return [];
|
|
}
|
|
throw new Error();
|
|
}
|
|
}
|
|
|
|
async getKnownPhishingDomains(): Promise<string[]> {
|
|
const response = await this.apiService.send("GET", "/phishing-domains", null, true, true);
|
|
return response as string[];
|
|
}
|
|
}
|