Skip to content
This repository has been archived by the owner on Jun 17, 2022. It is now read-only.

Commit

Permalink
Add new method for cycling through every login (#142)
Browse files Browse the repository at this point in the history
* Add new method for cycling through every login

To be used from browser extension when autofilling.
Related PR: bitwarden/clients#956

* Cache sorted ciphers by URL and invalidate them after a period of 5 seconds

* Move file to models
  • Loading branch information
xusoo committed Aug 12, 2020
1 parent e516692 commit 5c62938
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 6 deletions.
1 change: 1 addition & 0 deletions src/abstractions/cipher.service.ts
Expand Up @@ -24,6 +24,7 @@ export abstract class CipherService {
getAllDecryptedForUrl: (url: string, includeOtherTypes?: CipherType[]) => Promise<CipherView[]>;
getAllFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>;
getLastUsedForUrl: (url: string) => Promise<CipherView>;
getNextCipherForUrl: (url: string) => Promise<CipherView>;
updateLastUsedDate: (id: string) => Promise<void>;
saveNeverDomain: (domain: string) => Promise<void>;
saveWithServer: (cipher: Cipher) => Promise<any>;
Expand Down
60 changes: 60 additions & 0 deletions src/models/domain/sortedCiphersCache.ts
@@ -0,0 +1,60 @@
import { CipherView } from '../view';

const CacheTTL = 5000;

export class SortedCiphersCache {
private readonly sortedCiphersByUrl: Map<string, Ciphers> = new Map<string, Ciphers>();
private readonly timeouts: Map<string, any> = new Map<string, any>();

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];
}
}
27 changes: 21 additions & 6 deletions src/services/cipher.service.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -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<Cipher> {
Expand Down Expand Up @@ -437,13 +442,11 @@ export class CipherService implements CipherServiceAbstraction {
}

async getLastUsedForUrl(url: string): Promise<CipherView> {
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<CipherView> {
return this.getCipherForUrl(url, false);
}

async updateLastUsedDate(id: string): Promise<void> {
Expand Down Expand Up @@ -1002,4 +1005,16 @@ export class CipherService implements CipherServiceAbstraction {
throw new Error('Unknown cipher type.');
}
}

private async getCipherForUrl(url: string, lastUsed: boolean): Promise<CipherView> {
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);
}
}

0 comments on commit 5c62938

Please sign in to comment.