Add login using private key

This commit is contained in:
SondreB 2023-02-16 23:38:20 +01:00
parent 3fa1319d7b
commit 36a7712c4e
No known key found for this signature in database
GPG Key ID: D6CC44C75005FDBF
8 changed files with 246 additions and 1 deletions

View File

@ -24,6 +24,7 @@ import { DevelopmentComponent } from './development/development';
import { LoadingResolverService } from './services/loading-resolver';
import { NotificationsComponent } from './notifications/notifications';
import { FeedPrivateComponent } from './feed-private/feed-private';
import { ConnectKeyComponent } from './connect/key/key';
const routes: Routes = [
{
@ -38,6 +39,10 @@ const routes: Routes = [
path: 'connect',
component: ConnectComponent,
},
{
path: 'connect/key',
component: ConnectKeyComponent,
},
{
path: 'feed',
component: FeedPrivateComponent,

View File

@ -116,6 +116,7 @@ import { NotificationLabelComponent } from './shared/notification-label/notifica
import { RelayListComponent } from './shared/relay-list/relay-list';
import { AddRelayDialog } from './shared/add-relay-dialog/add-relay-dialog';
import { AddMediaDialog } from './queue/add-media-dialog/add-media-dialog';
import { ConnectKeyComponent } from './connect/key/key';
@NgModule({
declarations: [
@ -187,6 +188,7 @@ import { AddMediaDialog } from './queue/add-media-dialog/add-media-dialog';
NotificationLabelComponent,
RelayListComponent,
AddMediaDialog,
ConnectKeyComponent,
],
imports: [
AboutModule,

View File

@ -70,6 +70,18 @@
padding-bottom: 1.4em;
}
.dimmer {
color: rgba(0, 0, 0, 0.45);
}
.connect-input {
color: white !important;
}
/* .mat-form-field-appearance-legacy .mat-form-field-label {
color: white !important;
} */
.connect-content {
margin-top: 80px;
background: transparent url("/assets/bg.jpg") no-repeat right center;

View File

@ -59,7 +59,7 @@
<div *ngIf="consent">
<button class="start-button start-button-consent" *ngIf="consent" (click)="connect()" mat-flat-button>Connect using extension</button><br /><br />
<!-- <button class="start-button start-button-consent" *ngIf="consent" routerLink="/connect/key" mat-flat-button>Connect using private key</button><br /><br /> -->
<button class="start-button start-button-consent" *ngIf="consent" routerLink="/connect/key" mat-flat-button>Connect using private key</button><br /><br />
<button class="start-button start-button-consent" *ngIf="consent" (click)="readOnlyLogin = !readOnlyLogin" mat-flat-button>Connect using public key (read only)</button><br /><br />

View File

@ -0,0 +1,9 @@
.public-key {
margin-bottom: 1em;
word-wrap: break-word;
}
.error {
margin-bottom: 1em;
color: red;
}

View File

@ -0,0 +1,66 @@
<div class="connect-container">
<div class="connect-menu">
<img class="connect-logo" width="128" height="128" src="assets/icons/icon-256x256.webp" />
<div class="logo-text"><span class="hide-tiny">Blockcore</span> Notes</div>
<div class="connect-spacer"></div>
</div>
<div class="connect-content">
<mat-card class="card first-card">
<mat-card-content>
<h1>Private Key Import</h1>
<p>If you already have an existing private for your Nostr account, you can import it here and protect it with a password.</p>
<p>Having a strong password (we allow empty) is adviced, as this will be used to protect your private key using encryption when you are not using Blockcore Notes.</p>
<br />
<mat-form-field appearance="fill" class="input-full-width connect-input">
<mat-icon class="circle" matPrefix>person_add</mat-icon>
<mat-label>Private Key</mat-label>
<input (keyup)="updatePublicKey()" placeholder="nsec..." matInput type="text" autocomplete="off" [(ngModel)]="privateKey" />
</mat-form-field>
<mat-form-field appearance="fill" class="input-full-width connect-input">
<mat-icon class="circle" matPrefix>password</mat-icon>
<mat-label>Password (optional)</mat-label>
<input matInput type="password" autocomplete="off" [(ngModel)]="password" />
</mat-form-field>
<br />
<button [disabled]="!publicKey" class="start-button" (click)="persistKey()" mat-raised-button>Connect</button><br /><br />
<p *ngIf="!error" class="public-key dimmer"><strong>Public Key</strong>: {{ publicKey }}</p>
<p *ngIf="!error" class="public-key dimmer"><strong>Public Key (hex)</strong>: {{ publicKeyHex }}</p>
<p *ngIf="error" class="error">Error: {{ error }}</p>
<p class="dimmer">Remember that Blockcore cannot change or reset your password. Make sure you have a separate backup of your <strong>private key</strong> in case you loose your password.</p>
<p class="dimmer">You will be asked to enter password (if supplied) when Notes need to sign events on your behalf.</p>
<!-- <div><button class="skip-button" (click)="persistKey(privateKey)" color="primary" mat-raised-button>Connect (read-only)</button></div> -->
</mat-card-content>
</mat-card>
<!-- <mat-card class="card warn">
<mat-card-content>
<button class="start-button start-button-consent" *ngIf="consent" (click)="connect()" mat-flat-button>Connect using extension</button><br /><br />
<button class="start-button start-button-consent" *ngIf="consent" (click)="readOnlyLogin = !readOnlyLogin" mat-flat-button>Connect using public key (read only)</button><br /><br />
<div *ngIf="readOnlyLogin">
<div>
<div>
<p>Just paste your (or someone else's) Nostr public key (npub) here:</p>
<mat-form-field appearance="fill" class="input-full-width">
<mat-icon class="circle" matPrefix>person_add</mat-icon>
<mat-label>Public Key</mat-label>
<input matInput type="text" autocomplete="off" [(ngModel)]="readOnlyKey" />
</mat-form-field>
<div><button class="skip-button" (click)="anonymous(readOnlyKey)" color="primary" mat-raised-button>Connect (read-only)</button></div>
</div>
</div>
</div>
<div *ngIf="!consent" class="consent-required warn">You must agree with the notice below to enable login.</div>
<br />
</div>
</mat-card-content>
</mat-card> -->
</div>
</div>

150
src/app/connect/key/key.ts Normal file
View File

@ -0,0 +1,150 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { base64 } from '@scure/base';
import { relayInit, Relay, Event, utils, getPublicKey, nip19 } from 'nostr-tools';
const enc = new TextEncoder();
const dec = new TextDecoder();
@Component({
selector: 'app-key',
templateUrl: './key.html',
styleUrls: ['../connect.css', './key.css'],
})
export class ConnectKeyComponent {
privateKey: string = '';
privateKeyHex: string = '';
publicKey: string = '';
publicKeyHex: string = '';
password: string = '';
error: string = '';
constructor(private router: Router) {}
async persistKey() {
if (!this.privateKeyHex) {
return;
}
if (!this.publicKeyHex) {
return;
}
// First attempt to get public key from the private key to see if it's possible:
const encrypted = await this.encryptData(this.privateKey, this.password);
const decrypted = await this.decryptData(encrypted, this.password);
if (this.privateKey == decrypted) {
localStorage.setItem('blockcore:notes:nostr:prvkey', encrypted);
localStorage.setItem('blockcore:notes:nostr:pubkey', this.publicKeyHex);
this.router.navigateByUrl('/');
} else {
this.error = 'Unable to encrypt and decrypt. Cannot continue.';
console.error(this.error);
}
}
updatePublicKey() {
this.error = '';
this.publicKey = '';
this.privateKeyHex = '';
if (!this.privateKey) {
this.publicKey = '';
return;
}
if (this.privateKey.startsWith('npub')) {
this.error = 'The key value must be a "nsec" value. You entered "npub", which is your public key.';
return;
}
if (this.privateKey.startsWith('nsec')) {
this.privateKeyHex = nip19.decode(this.privateKey).data as any;
} else {
this.privateKeyHex = this.privateKey;
}
try {
this.publicKeyHex = getPublicKey(this.privateKeyHex);
this.publicKey = nip19.npubEncode(this.publicKeyHex);
} catch (err: any) {
this.error = err.message;
}
}
getPasswordKey(password: string) {
return window.crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveKey']);
}
deriveKey(passwordKey: any, salt: any, keyUsage: any) {
// TODO: Someone with better knowledge of cryptography should review our key sizes, iterations, etc.
return window.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 250000,
hash: 'SHA-256',
},
passwordKey,
{ name: 'AES-GCM', length: 256 },
false,
keyUsage
);
}
async encryptData(secretData: string, password: string) {
try {
const salt = window.crypto.getRandomValues(new Uint8Array(16));
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const passwordKey = await this.getPasswordKey(password);
const aesKey = await this.deriveKey(passwordKey, salt, ['encrypt']);
const encryptedContent = await window.crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv,
},
aesKey,
enc.encode(secretData)
);
const encryptedContentArr = new Uint8Array(encryptedContent);
let buff = new Uint8Array(salt.byteLength + iv.byteLength + encryptedContentArr.byteLength);
buff.set(salt, 0);
buff.set(iv, salt.byteLength);
buff.set(encryptedContentArr, salt.byteLength + iv.byteLength);
return base64.encode(buff);
} catch (e) {
console.error(e);
return '';
}
}
async decryptData(encryptedData: string, password: string) {
try {
const encryptedDataBuff = base64.decode(encryptedData);
const salt = encryptedDataBuff.slice(0, 16);
const iv = encryptedDataBuff.slice(16, 16 + 12);
const data = encryptedDataBuff.slice(16 + 12);
const passwordKey = await this.getPasswordKey(password.toString());
const aesKey = await this.deriveKey(passwordKey, salt, ['decrypt']);
const decryptedContent = await window.crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv,
},
aesKey,
data
);
return dec.decode(decryptedContent);
} catch (e) {
console.error(e);
return '';
}
}
}

View File

@ -52,6 +52,7 @@ export class AuthenticationService {
logout() {
localStorage.removeItem('blockcore:notes:nostr:pubkey');
localStorage.removeItem('blockcore:notes:nostr:prvkey');
this.authInfo$.next(AuthenticationService.UNKNOWN_USER);
this.router.navigateByUrl('/connect');
}