mirror of
https://github.com/block-core/blockcore-notes.git
synced 2024-09-29 06:20:42 +00:00
Add login using private key
This commit is contained in:
parent
3fa1319d7b
commit
36a7712c4e
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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 />
|
||||
|
||||
|
9
src/app/connect/key/key.css
Normal file
9
src/app/connect/key/key.css
Normal file
@ -0,0 +1,9 @@
|
||||
.public-key {
|
||||
margin-bottom: 1em;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-bottom: 1em;
|
||||
color: red;
|
||||
}
|
66
src/app/connect/key/key.html
Normal file
66
src/app/connect/key/key.html
Normal 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
150
src/app/connect/key/key.ts
Normal 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 '';
|
||||
}
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user