From df008fcace5bbf09eb3322ee2c3dc22da80c5160 Mon Sep 17 00:00:00 2001 From: Spencer Ofwiti Date: Fri, 11 Jun 2021 16:57:16 +0300 Subject: [PATCH 1/2] Refactor keystore into singleton service. --- src/app/_services/auth.service.ts | 131 ++++++++++----------- src/app/_services/index.ts | 2 + src/app/_services/keystore.service.spec.ts | 16 +++ src/app/_services/keystore.service.ts | 20 ++++ src/app/_services/token.service.ts | 6 +- src/app/_services/transaction.service.ts | 4 +- src/app/_services/user.service.ts | 3 +- src/app/auth/auth.component.ts | 8 +- 8 files changed, 114 insertions(+), 76 deletions(-) create mode 100644 src/app/_services/keystore.service.spec.ts create mode 100644 src/app/_services/keystore.service.ts diff --git a/src/app/_services/auth.service.ts b/src/app/_services/auth.service.ts index 937f2ce..9947631 100644 --- a/src/app/_services/auth.service.ts +++ b/src/app/_services/auth.service.ts @@ -3,12 +3,13 @@ import { hobaParseChallengeHeader } from '@src/assets/js/hoba.js'; import { signChallenge } from '@src/assets/js/hoba-pgp.js'; import { environment } from '@src/environments/environment'; import { LoggingService } from '@app/_services/logging.service'; -import { MutableKeyStore, MutablePgpKeyStore } from '@app/_pgp'; +import { MutableKeyStore } from '@app/_pgp'; import { ErrorDialogService } from '@app/_services/error-dialog.service'; import { HttpClient } from '@angular/common/http'; import { HttpError, rejectBody } from '@app/_helpers/global-error-handler'; import { Staff } from '@app/_models'; import { BehaviorSubject, Observable } from 'rxjs'; +import { KeystoreService } from '@app/_services/keystore.service'; @Injectable({ providedIn: 'root', @@ -25,17 +26,15 @@ export class AuthService { private httpClient: HttpClient, private loggingService: LoggingService, private errorDialogService: ErrorDialogService - ) { - this.mutableKeyStore = new MutablePgpKeyStore(); - } + ) {} async init(): Promise { - await this.mutableKeyStore.loadKeyring(); + this.mutableKeyStore = await KeystoreService.getKeystore(); if (localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))) { await this.mutableKeyStore.importPrivateKey(localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))); } } - + getSessionToken(): string { return sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN')); } @@ -49,84 +48,80 @@ export class AuthService { } getWithToken(): Promise { - const headers = { - Authorization: 'Bearer ' + this.getSessionToken, - 'Content-Type': 'application/json;charset=utf-8', - 'x-cic-automerge': 'none', - }; - const options = { - headers, - }; - return fetch(environment.cicMetaUrl, options).then((response) => { - if (!response.ok) { - this.loggingService.sendErrorLevelMessage('failed to get with auth token.', - this, - { error: "" }); + const headers = { + Authorization: 'Bearer ' + this.getSessionToken, + 'Content-Type': 'application/json;charset=utf-8', + 'x-cic-automerge': 'none', + }; + const options = { + headers, + }; + return fetch(environment.cicMetaUrl, options).then((response) => { + if (!response.ok) { + this.loggingService.sendErrorLevelMessage('failed to get with auth token.', this, { + error: '', + }); - return false; - } - return true; - }); + return false; + } + return true; + }); } // TODO rename to send signed challenge and set session. Also separate these responsibilities sendSignedChallenge(hobaResponseEncoded: any): Promise { - const headers = { - Authorization: 'HOBA ' + hobaResponseEncoded, - 'Content-Type': 'application/json;charset=utf-8', - 'x-cic-automerge': 'none', - }; - const options = { - headers, - }; - return fetch(environment.cicMetaUrl, options) + const headers = { + Authorization: 'HOBA ' + hobaResponseEncoded, + 'Content-Type': 'application/json;charset=utf-8', + 'x-cic-automerge': 'none', + }; + const options = { + headers, + }; + return fetch(environment.cicMetaUrl, options); } getChallenge(): Promise { - return fetch(environment.cicMetaUrl) - .then(response => { - if (response.status === 401) { - const authHeader: string = response.headers.get('WWW-Authenticate'); - return hobaParseChallengeHeader(authHeader); - } - }); + return fetch(environment.cicMetaUrl).then((response) => { + if (response.status === 401) { + const authHeader: string = response.headers.get('WWW-Authenticate'); + return hobaParseChallengeHeader(authHeader); + } + }); } async login(): Promise { if (this.getSessionToken()) { - sessionStorage.removeItem(btoa('CICADA_SESSION_TOKEN')); + sessionStorage.removeItem(btoa('CICADA_SESSION_TOKEN')); } else { - const o = await this.getChallenge(); + const o = await this.getChallenge(); - const r = await signChallenge( - o.challenge, - o.realm, - environment.cicMetaUrl, - this.mutableKeyStore - ); + const r = await signChallenge( + o.challenge, + o.realm, + environment.cicMetaUrl, + this.mutableKeyStore + ); - const tokenResponse = await this.sendSignedChallenge(r) - .then(response => { - const token = response.headers.get('Token') - if (token) { - return token - } - if (response.status === 401) { - let e = new HttpError("You are not authorized to use this system", response.status) - throw e - } - if (!response.ok) { - let e = new HttpError("Unknown error from authentication server", response.status) - throw e - } - }) - - if (tokenResponse) { - this.setSessionToken(tokenResponse); - this.setState('Click button to log in'); - return true + const tokenResponse = await this.sendSignedChallenge(r).then((response) => { + const token = response.headers.get('Token'); + if (token) { + return token; } - return false + if (response.status === 401) { + throw new HttpError('You are not authorized to use this system', response.status); + } + if (!response.ok) { + throw new HttpError('Unknown error from authentication server', response.status); + } + }); + + if (tokenResponse) { + this.setSessionToken(tokenResponse); + this.setState('Click button to log in'); + return true; + } + return false; } } diff --git a/src/app/_services/index.ts b/src/app/_services/index.ts index 88db87d..13aefd9 100644 --- a/src/app/_services/index.ts +++ b/src/app/_services/index.ts @@ -7,3 +7,5 @@ export * from '@app/_services/location.service'; export * from '@app/_services/logging.service'; export * from '@app/_services/error-dialog.service'; export * from '@app/_services/web3.service'; +export * from '@app/_services/keystore.service'; +export * from '@app/_services/registry.service'; diff --git a/src/app/_services/keystore.service.spec.ts b/src/app/_services/keystore.service.spec.ts new file mode 100644 index 0000000..b0a249b --- /dev/null +++ b/src/app/_services/keystore.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { KeystoreService } from './keystore.service'; + +describe('KeystoreService', () => { + let service: KeystoreService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(KeystoreService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/_services/keystore.service.ts b/src/app/_services/keystore.service.ts new file mode 100644 index 0000000..6fcaa37 --- /dev/null +++ b/src/app/_services/keystore.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { MutableKeyStore, MutablePgpKeyStore } from '@app/_pgp'; + +@Injectable({ + providedIn: 'root', +}) +export class KeystoreService { + private static mutableKeyStore: MutableKeyStore; + + constructor() {} + + public static async getKeystore(): Promise { + if (!KeystoreService.mutableKeyStore) { + this.mutableKeyStore = new MutablePgpKeyStore(); + await this.mutableKeyStore.loadKeyring(); + return KeystoreService.mutableKeyStore; + } + return KeystoreService.mutableKeyStore; + } +} diff --git a/src/app/_services/token.service.ts b/src/app/_services/token.service.ts index d9ffed8..292b417 100644 --- a/src/app/_services/token.service.ts +++ b/src/app/_services/token.service.ts @@ -4,7 +4,7 @@ import { TokenRegistry } from '@app/_eth'; import { HttpClient } from '@angular/common/http'; import { RegistryService } from '@app/_services/registry.service'; import { Token } from '@app/_models'; -import {BehaviorSubject, Observable, Subject} from 'rxjs'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; @Injectable({ providedIn: 'root', @@ -14,7 +14,9 @@ export class TokenService { tokenRegistry: TokenRegistry; onload: (status: boolean) => void; tokens: Array = []; - private tokensList: BehaviorSubject> = new BehaviorSubject>(this.tokens); + private tokensList: BehaviorSubject> = new BehaviorSubject>( + this.tokens + ); tokensSubject: Observable> = this.tokensList.asObservable(); constructor(private httpClient: HttpClient) {} diff --git a/src/app/_services/transaction.service.ts b/src/app/_services/transaction.service.ts index 5a6c7d8..6c09320 100644 --- a/src/app/_services/transaction.service.ts +++ b/src/app/_services/transaction.service.ts @@ -18,6 +18,7 @@ import { CICRegistry } from 'cic-client'; import { RegistryService } from '@app/_services/registry.service'; import Web3 from 'web3'; import { Web3Service } from '@app/_services/web3.service'; +import { KeystoreService } from '@app/_services/keystore.service'; const vCard = require('vcard-parser'); @Injectable({ @@ -170,7 +171,8 @@ export class TransactionService { tx.value = toValue(value); tx.data = data; const txMsg = tx.message(); - const privateKey = this.authService.mutableKeyStore.getPrivateKey(); + const keystore = await KeystoreService.getKeystore(); + const privateKey = keystore.getPrivateKey(); if (!privateKey.isDecrypted()) { const password = window.prompt('password'); await privateKey.decrypt(password); diff --git a/src/app/_services/user.service.ts b/src/app/_services/user.service.ts index e1db2d5..9b83503 100644 --- a/src/app/_services/user.service.ts +++ b/src/app/_services/user.service.ts @@ -14,6 +14,7 @@ import { CICRegistry } from 'cic-client'; import { AuthService } from '@app/_services/auth.service'; import { personValidation, vcardValidation } from '@app/_helpers'; import { add0x } from '@src/assets/js/ethtx/dist/hex'; +import { KeystoreService } from '@app/_services/keystore.service'; const vCard = require('vcard-parser'); @Injectable({ @@ -45,7 +46,7 @@ export class UserService { async init(): Promise { await this.authService.init(); await this.tokenService.init(); - this.keystore = this.authService.mutableKeyStore; + this.keystore = await KeystoreService.getKeystore(); this.signer = new PGPSigner(this.keystore); this.registry = await RegistryService.getRegistry(); } diff --git a/src/app/auth/auth.component.ts b/src/app/auth/auth.component.ts index 328997f..4272cec 100644 --- a/src/app/auth/auth.component.ts +++ b/src/app/auth/auth.component.ts @@ -22,7 +22,7 @@ export class AuthComponent implements OnInit { private authService: AuthService, private formBuilder: FormBuilder, private router: Router, - private errorDialogService: ErrorDialogService, + private errorDialogService: ErrorDialogService ) {} async ngOnInit(): Promise { @@ -49,10 +49,10 @@ export class AuthComponent implements OnInit { async login(): Promise { try { - const loginResult = await this.authService.login() - if (loginResult) { + const loginResult = await this.authService.login(); + if (loginResult) { this.router.navigate(['/home']); - } + } } catch (HttpError) { this.errorDialogService.openDialog({ message: HttpError.message, From 85e41e99b04f1c6b9b30108bb7d656cb9791fdb3 Mon Sep 17 00:00:00 2001 From: Spencer Ofwiti Date: Mon, 14 Jun 2021 17:50:39 +0300 Subject: [PATCH 2/2] Add promises to keystore singleton service. --- src/app/_services/keystore.service.ts | 14 ++++++++------ src/environments/environment.dev.ts | 2 +- src/environments/environment.prod.ts | 2 +- src/environments/environment.ts | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/app/_services/keystore.service.ts b/src/app/_services/keystore.service.ts index 6fcaa37..7d8b92e 100644 --- a/src/app/_services/keystore.service.ts +++ b/src/app/_services/keystore.service.ts @@ -10,11 +10,13 @@ export class KeystoreService { constructor() {} public static async getKeystore(): Promise { - if (!KeystoreService.mutableKeyStore) { - this.mutableKeyStore = new MutablePgpKeyStore(); - await this.mutableKeyStore.loadKeyring(); - return KeystoreService.mutableKeyStore; - } - return KeystoreService.mutableKeyStore; + return new Promise(async (resolve, reject) => { + if (!KeystoreService.mutableKeyStore) { + this.mutableKeyStore = new MutablePgpKeyStore(); + await this.mutableKeyStore.loadKeyring(); + return resolve(KeystoreService.mutableKeyStore); + } + return resolve(KeystoreService.mutableKeyStore); + }); } } diff --git a/src/environments/environment.dev.ts b/src/environments/environment.dev.ts index 978f71f..cde24f3 100644 --- a/src/environments/environment.dev.ts +++ b/src/environments/environment.dev.ts @@ -6,7 +6,7 @@ export const environment = { logLevel: NgxLoggerLevel.ERROR, serverLogLevel: NgxLoggerLevel.OFF, loggingUrl: '', - cicMetaUrl: 'https://meta.dev.grassrootseconomics.net', + cicMetaUrl: 'https://meta-auth.dev.grassrootseconomics.net', publicKeysUrl: 'https://dev.grassrootseconomics.net/.well-known/publickeys/', cicCacheUrl: 'https://cache.dev.grassrootseconomics.net', web3Provider: 'wss://bloxberg-ws.dev.grassrootseconomics.net', diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index f4786c1..32cc50b 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -6,7 +6,7 @@ export const environment = { logLevel: NgxLoggerLevel.ERROR, serverLogLevel: NgxLoggerLevel.OFF, loggingUrl: '', - cicMetaUrl: 'https://meta.dev.grassrootseconomics.net', + cicMetaUrl: 'https://meta-auth.dev.grassrootseconomics.net', publicKeysUrl: 'https://dev.grassrootseconomics.net/.well-known/publickeys/', cicCacheUrl: 'https://cache.dev.grassrootseconomics.net', web3Provider: 'wss://bloxberg-ws.dev.grassrootseconomics.net', diff --git a/src/environments/environment.ts b/src/environments/environment.ts index df02af7..f2d7c4f 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -6,7 +6,7 @@ export const environment = { logLevel: NgxLoggerLevel.ERROR, serverLogLevel: NgxLoggerLevel.OFF, loggingUrl: 'http://localhost:8000', - cicMetaUrl: 'https://meta.dev.grassrootseconomics.net', + cicMetaUrl: 'https://meta-auth.dev.grassrootseconomics.net', publicKeysUrl: 'https://dev.grassrootseconomics.net/.well-known/publickeys/', cicCacheUrl: 'https://cache.dev.grassrootseconomics.net', web3Provider: 'wss://bloxberg-ws.dev.grassrootseconomics.net',