Compare commits

...

13 Commits

Author SHA1 Message Date
Spencer Ofwiti 8e96b070a5 Lint code. 2021-06-07 18:49:15 +03:00
Spencer Ofwiti 159cedc86e Merge branch 'master' into spencer/censor-pgp-passphrase
# Conflicts:
#	package-lock.json
#	src/app/_helpers/http-getter.ts
#	src/app/_services/auth.service.ts
#	src/app/auth/auth.component.ts
#	src/assets/js/hoba-pgp.js
#	src/styles.scss
2021-06-07 18:44:26 +03:00
Spencer Ofwiti f494a32e20 Merge branch 'master' into spencer/censor-pgp-passphrase 2021-04-28 15:56:20 +03:00
Spencer Ofwiti 63cff19bae Escalate errors to higher level handlers. 2021-04-21 12:37:40 +03:00
Spencer Ofwiti 53ed56460c Fix form alignment. 2021-04-21 12:03:09 +03:00
Spencer Ofwiti 632669ddf4 Merge branch 'master' into spencer/censor-pgp-passphrase 2021-04-19 15:25:30 +03:00
Spencer Ofwiti ad3767017b Refactor get challenge method. 2021-04-19 15:22:44 +03:00
Spencer Ofwiti a9ad7012d8 Remove unnecessary logging. 2021-04-17 14:37:59 +03:00
Spencer Ofwiti 8c08607a3a Refactor httpGetter. 2021-04-17 14:02:29 +03:00
Spencer Ofwiti 2116a55549 Refactor AJAX to use fetch API. 2021-04-17 13:31:07 +03:00
Spencer Ofwiti 94da4baceb Merge branch 'master' into spencer/censor-pgp-passphrase 2021-04-17 11:53:34 +03:00
Spencer Ofwiti 80815192f1 Bug fix. 2021-04-06 17:00:49 +03:00
Spencer Ofwiti d72629921a Add separate pgp passphrase entry page. 2021-04-06 15:58:07 +03:00
6 changed files with 179 additions and 63 deletions

View File

@ -28,7 +28,7 @@ interface MutableKeyStore extends KeyStore {
removePublicKeyForId(keyId: string): any; removePublicKeyForId(keyId: string): any;
removePublicKey(publicKey: any): any; removePublicKey(publicKey: any): any;
clearKeysInKeyring(): void; clearKeysInKeyring(): void;
sign(plainText: string): Promise<any>; sign(plainText: string, passphrase: string): Promise<any>;
} }
class MutablePgpKeyStore implements MutableKeyStore { class MutablePgpKeyStore implements MutableKeyStore {
@ -150,11 +150,10 @@ class MutablePgpKeyStore implements MutableKeyStore {
keyring.clear(); keyring.clear();
} }
async sign(plainText): Promise<any> { async sign(plainText: string, passphrase: string): Promise<any> {
const privateKey = this.getPrivateKey(); const privateKey = this.getPrivateKey();
if (!privateKey.isDecrypted()) { if (!privateKey.isDecrypted()) {
const password = window.prompt('password'); await privateKey.decrypt(passphrase);
await privateKey.decrypt(password);
} }
const opts = { const opts = {
message: openpgp.message.fromText(plainText), message: openpgp.message.fromText(plainText),

View File

@ -13,6 +13,7 @@ import { HttpError, rejectBody } from '@app/_helpers/global-error-handler';
}) })
export class AuthService { export class AuthService {
sessionToken: any; sessionToken: any;
privateKey: any;
mutableKeyStore: MutableKeyStore; mutableKeyStore: MutableKeyStore;
constructor( constructor(
@ -30,6 +31,7 @@ export class AuthService {
this.sessionToken = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN')); this.sessionToken = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'));
} }
if (localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))) { if (localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))) {
this.privateKey = localStorage.getItem(btoa('CICADA_PRIVATE_KEY'));
await this.mutableKeyStore.importPrivateKey(localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))); await this.mutableKeyStore.importPrivateKey(localStorage.getItem(btoa('CICADA_PRIVATE_KEY')));
} }
} }
@ -94,21 +96,32 @@ export class AuthService {
}); });
} }
async passwordLogin(password: string): Promise<boolean> {
try {
const o = await this.getChallenge();
await this.loginResponse(o, password);
return true;
} catch (error) {
this.loggingService.sendErrorLevelMessage(
`Login challenge failed: Error ${error.status} - ${error.statusText}`,
this,
{ error }
);
}
return false;
}
async login(): Promise<boolean> { async login(): Promise<boolean> {
if (this.sessionToken !== undefined) { if (this.sessionToken !== undefined) {
try { try {
const response: boolean = await this.getWithToken(); this.getWithToken();
return response === true; return true;
} catch (e) { } catch (error) {
this.loggingService.sendErrorLevelMessage('Login token failed', this, { error: e }); this.loggingService.sendErrorLevelMessage(
} `Login token failed: Error ${error.status} - ${error.statusText}`,
} else { this,
try { { error }
const o = await this.getChallenge(); );
const response: boolean = await this.loginResponse(o);
return response === true;
} catch (e) {
this.loggingService.sendErrorLevelMessage('Login challenge failed', this, { error: e });
} }
} }
return false; return false;
@ -149,23 +162,18 @@ export class AuthService {
}); });
} }
loginView(): void {
document.getElementById('one').style.display = 'none';
document.getElementById('two').style.display = 'block';
this.setState('Click button to log in with PGP key ' + this.mutableKeyStore.getPrivateKeyId());
}
async setKey(privateKeyArmored): Promise<boolean> { async setKey(privateKeyArmored): Promise<boolean> {
try { try {
const isValidKeyCheck = await this.mutableKeyStore.isValidKey(privateKeyArmored); const isValidKeyCheck = await this.mutableKeyStore.isValidKey(privateKeyArmored);
if (!isValidKeyCheck) { if (!isValidKeyCheck) {
throw Error('The private key is invalid'); throw Error('The private key is invalid');
} }
// TODO leaving this out for now. const isEncryptedKeyCheck = await this.mutableKeyStore.isEncryptedPrivateKey(
// const isEncryptedKeyCheck = await this.mutableKeyStore.isEncryptedPrivateKey(privateKeyArmored); privateKeyArmored
// if (!isEncryptedKeyCheck) { );
// throw Error('The private key doesn\'t have a password!'); if (!isEncryptedKeyCheck) {
// } throw Error('The private key does not have a password!');
}
const key = await this.mutableKeyStore.importPrivateKey(privateKeyArmored); const key = await this.mutableKeyStore.importPrivateKey(privateKeyArmored);
localStorage.setItem(btoa('CICADA_PRIVATE_KEY'), privateKeyArmored); localStorage.setItem(btoa('CICADA_PRIVATE_KEY'), privateKeyArmored);
} catch (err) { } catch (err) {
@ -179,7 +187,6 @@ export class AuthService {
}); });
return false; return false;
} }
this.loginView();
return true; return true;
} }

View File

@ -7,7 +7,7 @@
<h1 class="text-white">CICADA</h1> <h1 class="text-white">CICADA</h1>
</a> </a>
</mat-card-title> </mat-card-title>
<div id="one" style="display: block" class="card-body p-4"> <div id="one" style="display: block" class="card-body p-4 align-items-center">
<div class="text-center w-75 m-auto"> <div class="text-center w-75 m-auto">
<h4 class="text-dark-50 text-center font-weight-bold">Add Private Key</h4> <h4 class="text-dark-50 text-center font-weight-bold">Add Private Key</h4>
@ -19,13 +19,13 @@
<mat-label>Private Key</mat-label> <mat-label>Private Key</mat-label>
<textarea matInput style="height: 30rem" formControlName="key" placeholder="Enter your private key..." <textarea matInput style="height: 30rem" formControlName="key" placeholder="Enter your private key..."
[errorStateMatcher]="matcher"></textarea> [errorStateMatcher]="matcher"></textarea>
<div *ngIf="submitted && keyFormStub.key.errors" class="invalid-feedback"> <div *ngIf="keyFormSubmitted && keyFormStub.key.errors" class="invalid-feedback">
<mat-error *ngIf="keyFormStub.key.errors.required">Private Key is required.</mat-error> <mat-error *ngIf="keyFormStub.key.errors.required">Private Key is required.</mat-error>
</div> </div>
</mat-form-field> </mat-form-field>
<button mat-raised-button matRipple color="primary" type="submit" [disabled]="loading"> <button mat-raised-button matRipple color="primary" type="submit" [disabled]="keyFormLoading">
<span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span> <span *ngIf="keyFormLoading" class="spinner-border spinner-border-sm mr-1"></span>
Add Key Add Key
</button> </button>
@ -33,6 +33,43 @@
</div> </div>
<div id="two" style="display: none" class="card-body p-4 align-items-center"> <div id="two" style="display: none" class="card-body p-4 align-items-center">
<div class="text-center w-75 m-auto">
<h4 id="passwordState" class="text-dark-50 text-center font-weight-bold"></h4>
</div>
<div class="center-items">
<form [formGroup]="passwordForm" (ngSubmit)="onPasswordInput()">
<mat-form-field appearance="outline">
<mat-label>Password</mat-label>
<input matInput type="password" formControlName="password" placeholder="Enter your private key password..."
[errorStateMatcher]="matcher">
<div *ngIf="passwordForm && passwordFormStub.password.errors" class="invalid-feedback">
<mat-error *ngIf="passwordFormStub.password.errors.required">Private Key password is required.</mat-error>
</div>
</mat-form-field>
<button id="passwordLoginButton" mat-raised-button matRipple color="primary" type="submit" class="ml-3" [disabled]="passwordFormLoading">
<span *ngIf="passwordFormLoading" class="spinner-border spinner-border-sm mr-1"></span>
Login
</button>
</form>
</div>
<div class="row mt-3">
<div class="col-12 text-center">
<p class="text-muted">Change private key?
<a (click)="keyInput()" class="text-muted ml-1">
<b>Enter private key</b>
</a>
</p>
</div> <!-- end col-->
</div>
<!-- end row -->
</div>
<div id="three" style="display: none" class="card-body p-4 align-items-center">
<div class="text-center w-75 m-auto"> <div class="text-center w-75 m-auto">
<h4 id="state" class="text-dark-50 text-center font-weight-bold"></h4> <h4 id="state" class="text-dark-50 text-center font-weight-bold"></h4>
<button mat-raised-button matRipple color="primary" type="submit" (click)="login()"> Login </button> <button mat-raised-button matRipple color="primary" type="submit" (click)="login()"> Login </button>
@ -40,7 +77,11 @@
<div class="row mt-3"> <div class="row mt-3">
<div class="col-12 text-center"> <div class="col-12 text-center">
<p class="text-muted">Change private key? <a (click)="switchWindows()" class="text-muted ml-1"><b>Enter private key</b></a></p> <p class="text-muted">Change private key?
<a (click)="keyInput()" class="text-muted ml-1">
<b>Enter private key</b>
</a>
</p>
</div> <!-- end col--> </div> <!-- end col-->
</div> </div>
<!-- end row --> <!-- end row -->

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { CustomErrorStateMatcher } from '@app/_helpers'; import { CustomErrorStateMatcher } from '@app/_helpers';
import { AuthService } from '@app/_services'; import { AuthService } from '@app/_services';
@ -12,68 +12,126 @@ import { Router } from '@angular/router';
}) })
export class AuthComponent implements OnInit { export class AuthComponent implements OnInit {
keyForm: FormGroup; keyForm: FormGroup;
submitted: boolean = false;
loading: boolean = false;
matcher: CustomErrorStateMatcher = new CustomErrorStateMatcher(); matcher: CustomErrorStateMatcher = new CustomErrorStateMatcher();
keyFormSubmitted: boolean = false;
keyFormLoading: boolean = false;
passwordForm: FormGroup;
passwordFormSubmitted: boolean = false;
passwordFormLoading: boolean = false;
constructor( constructor(
private authService: AuthService, private authService: AuthService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private router: Router private router: Router,
private cdr: ChangeDetectorRef
) {} ) {}
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
this.keyForm = this.formBuilder.group({ this.keyForm = this.formBuilder.group({
key: ['', Validators.required], key: ['', Validators.required],
}); });
await this.authService.init(); this.passwordForm = this.formBuilder.group({
// if (this.authService.privateKey !== undefined) { password: ['', Validators.required],
// const setKey = await this.authService.setKey(this.authService.privateKey); });
// } if (this.authService.privateKey !== undefined) {
// } const setKey = await this.authService.setKey(this.authService.privateKey);
if (setKey) {
this.passwordInput();
}
if (setKey && this.authService.sessionToken !== undefined) {
this.loginView();
}
}
} }
get keyFormStub(): any { get keyFormStub(): any {
return this.keyForm.controls; return this.keyForm.controls;
} }
get passwordFormStub(): any {
return this.passwordForm.controls;
}
async onSubmit(): Promise<void> { async onSubmit(): Promise<void> {
this.submitted = true; this.keyFormSubmitted = true;
if (this.keyForm.invalid) { if (this.keyForm.invalid) {
return; return;
} }
this.loading = true; this.keyFormLoading = true;
await this.authService.setKey(this.keyFormStub.key.value); const keySetup = await this.authService.setKey(this.keyFormStub.key.value);
this.loading = false; if (keySetup) {
this.passwordInput();
}
this.keyFormLoading = false;
this.cdr.detectChanges();
}
async onPasswordInput(): Promise<void> {
this.passwordFormSubmitted = true;
if (this.passwordForm.invalid) {
return;
}
this.passwordFormLoading = true;
const passwordLogin = await this.authService.passwordLogin(
this.passwordFormStub.password.value
);
if (passwordLogin) {
this.loginView();
}
this.passwordFormLoading = false;
this.cdr.detectChanges();
} }
login(): void { login(): void {
// TODO check if we have privatekey if (this.authService.sessionToken === undefined) {
// Send us to home if we have a private key this.passwordInput();
// talk to meta somehow }
// in the error interceptor if 401/403 handle it const loginStatus = this.authService.login();
// if 200 go /home if (loginStatus) {
if (this.authService.getPrivateKey()) {
this.router.navigate(['/home']); this.router.navigate(['/home']);
} }
} }
switchWindows(): void { keyInput(): void {
this.authService.sessionToken = undefined; this.authService.sessionToken = undefined;
const divOne: HTMLElement = document.getElementById('one'); this.switchWindows(true, false, false);
const divTwo: HTMLElement = document.getElementById('two');
this.toggleDisplay(divOne);
this.toggleDisplay(divTwo);
} }
toggleDisplay(element: any): void { passwordInput(): void {
this.authService.sessionToken = undefined;
this.switchWindows(false, true, false);
this.setPasswordState(
'Enter Password to log in with PGP key ' + this.authService.mutableKeyStore.getPrivateKeyId()
);
}
loginView(): void {
this.switchWindows(false, false, true);
this.authService.setState('Click button to log in');
}
switchWindows(divOneStatus: boolean, divTwoStatus: boolean, divThreeStatus: boolean): void {
const divOne = document.getElementById('one');
const divTwo = document.getElementById('two');
const divThree = document.getElementById('three');
this.toggleDisplay(divOne, divOneStatus);
this.toggleDisplay(divTwo, divTwoStatus);
this.toggleDisplay(divThree, divThreeStatus);
}
toggleDisplay(element: any, active: boolean): void {
const style: string = window.getComputedStyle(element).display; const style: string = window.getComputedStyle(element).display;
if (style === 'block') { if (active) {
element.style.display = 'none';
} else {
element.style.display = 'block'; element.style.display = 'block';
} else {
element.style.display = 'none';
} }
} }
setPasswordState(s): void {
document.getElementById('passwordState').innerHTML = s;
}
} }

View File

@ -2,7 +2,7 @@ import { hobaResult, hobaToSign } from '@src/assets/js/hoba.js';
const alg = '969'; const alg = '969';
export async function signChallenge(challenge, realm, origin, keyStore) { export async function signChallenge(challenge, realm, origin, keyStore, password) {
const fingerprint = keyStore.getFingerprint(); const fingerprint = keyStore.getFingerprint();
const nonce_array = new Uint8Array(32); const nonce_array = new Uint8Array(32);
crypto.getRandomValues(nonce_array); crypto.getRandomValues(nonce_array);
@ -14,7 +14,7 @@ export async function signChallenge(challenge, realm, origin, keyStore) {
const a_challenge = btoa(challenge); const a_challenge = btoa(challenge);
const message = hobaToSign(a_nonce, a_kid, a_challenge, realm, origin, alg); const message = hobaToSign(a_nonce, a_kid, a_challenge, realm, origin, alg);
const signature = await keyStore.sign(message); const signature = await keyStore.sign(message, password);
const a_signature = btoa(signature); const a_signature = btoa(signature);
const result = hobaResult(a_nonce, a_kid, a_challenge, a_signature); const result = hobaResult(a_nonce, a_kid, a_challenge, a_signature);

View File

@ -65,6 +65,12 @@ footer.footer { color: black; }
left: -9999px; left: -9999px;
} }
.center-items {
display: flex;
justify-content: center;
align-items: center;
}
#sidebar { #sidebar {
position: sticky; position: sticky;
position: -webkit-sticky; position: -webkit-sticky;
@ -207,6 +213,11 @@ a[data-toggle="collapse"] { position: relative; }
.mat-column-select { overflow: initial; } .mat-column-select { overflow: initial; }
#passwordLoginButton {
height: 3.0rem;
padding-top: 0.5rem;
}
button { height: 2.5rem; } button { height: 2.5rem; }
.badge-pill { width: 5rem; } .badge-pill { width: 5rem; }