From 5c62938dbbf3fe4146bf18a10827cd51a4157049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Mar=C3=AD?= Date: Wed, 12 Aug 2020 21:59:59 +0200 Subject: [PATCH] Add new method for cycling through every login (#142) * Add new method for cycling through every login To be used from browser extension when autofilling. Related PR: https://github.com/bitwarden/browser/pull/956 * Cache sorted ciphers by URL and invalidate them after a period of 5 seconds * Move file to models --- src/abstractions/cipher.service.ts | 1 + src/models/domain/sortedCiphersCache.ts | 60 +++++++++++++++++++++++++ src/services/cipher.service.ts | 27 ++++++++--- 3 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 src/models/domain/sortedCiphersCache.ts diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index 3c55b2cdbf..8ec886e240 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -24,6 +24,7 @@ export abstract class CipherService { getAllDecryptedForUrl: (url: string, includeOtherTypes?: CipherType[]) => Promise; getAllFromApiForOrganization: (organizationId: string) => Promise; getLastUsedForUrl: (url: string) => Promise; + getNextCipherForUrl: (url: string) => Promise; updateLastUsedDate: (id: string) => Promise; saveNeverDomain: (domain: string) => Promise; saveWithServer: (cipher: Cipher) => Promise; diff --git a/src/models/domain/sortedCiphersCache.ts b/src/models/domain/sortedCiphersCache.ts new file mode 100644 index 0000000000..6976904d9d --- /dev/null +++ b/src/models/domain/sortedCiphersCache.ts @@ -0,0 +1,60 @@ +import { CipherView } from '../view'; + +const CacheTTL = 5000; + +export class SortedCiphersCache { + private readonly sortedCiphersByUrl: Map = new Map(); + private readonly timeouts: Map = new Map(); + + constructor(private readonly comparator: (a: CipherView, b: CipherView) => number) { } + + isCached(url: string) { + return this.sortedCiphersByUrl.has(url); + } + + addCiphers(url: string, ciphers: CipherView[]) { + ciphers.sort(this.comparator); + this.sortedCiphersByUrl.set(url, new Ciphers(ciphers)); + this.resetTimer(url); + } + + getLastUsed(url: string) { + this.resetTimer(url); + return this.isCached(url) ? this.sortedCiphersByUrl.get(url).getLastUsed() : null; + } + + getNext(url: string) { + this.resetTimer(url); + return this.isCached(url) ? this.sortedCiphersByUrl.get(url).getNext() : null; + } + + clear() { + this.sortedCiphersByUrl.clear(); + this.timeouts.clear(); + } + + private resetTimer(url: string) { + clearTimeout(this.timeouts.get(url)); + this.timeouts.set(url, setTimeout(() => { + this.sortedCiphersByUrl.delete(url); + this.timeouts.delete(url); + }, CacheTTL)); + } +} + +class Ciphers { + lastUsedIndex = -1; + + constructor(private readonly ciphers: CipherView[]) { } + + getLastUsed() { + this.lastUsedIndex = Math.max(this.lastUsedIndex, 0); + return this.ciphers[this.lastUsedIndex]; + } + + getNext() { + const nextIndex = (this.lastUsedIndex + 1) % this.ciphers.length; + this.lastUsedIndex = nextIndex; + return this.ciphers[nextIndex]; + } +} diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 5145f519b5..fe661bdeea 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -35,6 +35,8 @@ import { FieldView } from '../models/view/fieldView'; import { PasswordHistoryView } from '../models/view/passwordHistoryView'; import { View } from '../models/view/view'; +import { SortedCiphersCache } from '../models/domain/sortedCiphersCache'; + import { ApiService } from '../abstractions/api.service'; import { CipherService as CipherServiceAbstraction } from '../abstractions/cipher.service'; import { CryptoService } from '../abstractions/crypto.service'; @@ -63,6 +65,8 @@ export class CipherService implements CipherServiceAbstraction { // tslint:disable-next-line _decryptedCipherCache: CipherView[]; + private sortedCiphersCache: SortedCiphersCache = new SortedCiphersCache(this.sortCiphersByLastUsed); + constructor(private cryptoService: CryptoService, private userService: UserService, private settingsService: SettingsService, private apiService: ApiService, private storageService: StorageService, private i18nService: I18nService, @@ -85,6 +89,7 @@ export class CipherService implements CipherServiceAbstraction { clearCache(): void { this.decryptedCipherCache = null; + this.sortedCiphersCache.clear(); } async encrypt(model: CipherView, key?: SymmetricCryptoKey, originalCipher: Cipher = null): Promise { @@ -437,13 +442,11 @@ export class CipherService implements CipherServiceAbstraction { } async getLastUsedForUrl(url: string): Promise { - const ciphers = await this.getAllDecryptedForUrl(url); - if (ciphers.length === 0) { - return null; - } + return this.getCipherForUrl(url, true); + } - const sortedCiphers = ciphers.sort(this.sortCiphersByLastUsed); - return sortedCiphers[0]; + async getNextCipherForUrl(url: string): Promise { + return this.getCipherForUrl(url, false); } async updateLastUsedDate(id: string): Promise { @@ -1002,4 +1005,16 @@ export class CipherService implements CipherServiceAbstraction { throw new Error('Unknown cipher type.'); } } + + private async getCipherForUrl(url: string, lastUsed: boolean): Promise { + if (!this.sortedCiphersCache.isCached(url)) { + const ciphers = await this.getAllDecryptedForUrl(url); + if (!ciphers) { + return null; + } + this.sortedCiphersCache.addCiphers(url, ciphers); + } + + return lastUsed ? this.sortedCiphersCache.getLastUsed(url) : this.sortedCiphersCache.getNext(url); + } }