Release v1.1.3 - Add NIP-49 ncryptsec export feature
- Add ncryptsec page for exporting encrypted private keys (NIP-49) - Implement password-based encryption using scrypt + XChaCha20-Poly1305 - Display QR code for easy mobile scanning of encrypted key - Add click-to-copy functionality for ncryptsec string - Add privkeyToNcryptsec() method to NostrHelper using nostr-tools nip49 Files modified: - projects/common/src/lib/helpers/nostr-helper.ts - projects/chrome/src/app/app.routes.ts - projects/chrome/src/app/components/edit-identity/keys/keys.component.* - projects/chrome/src/app/components/edit-identity/ncryptsec/ (new) - projects/firefox/src/app/app.routes.ts - projects/firefox/src/app/components/edit-identity/keys/keys.component.* - projects/firefox/src/app/components/edit-identity/ncryptsec/ (new) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "plebeian-signer",
|
"name": "plebeian-signer",
|
||||||
"version": "v1.1.2",
|
"version": "v1.1.3",
|
||||||
"custom": {
|
"custom": {
|
||||||
"chrome": {
|
"chrome": {
|
||||||
"version": "v1.1.2"
|
"version": "v1.1.3"
|
||||||
},
|
},
|
||||||
"firefox": {
|
"firefox": {
|
||||||
"version": "v1.1.2"
|
"version": "v1.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Plebeian Signer - Nostr Identity Manager & Signer",
|
"name": "Plebeian Signer - Nostr Identity Manager & Signer",
|
||||||
"description": "Manage and switch between multiple identities while interacting with Nostr apps",
|
"description": "Manage and switch between multiple identities while interacting with Nostr apps",
|
||||||
"version": "1.1.2",
|
"version": "1.1.3",
|
||||||
"homepage_url": "https://github.com/PlebeianApp/plebeian-signer",
|
"homepage_url": "https://github.com/PlebeianApp/plebeian-signer",
|
||||||
"options_page": "options.html",
|
"options_page": "options.html",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { NewIdentityComponent } from './components/new-identity/new-identity.com
|
|||||||
import { EditIdentityComponent } from './components/edit-identity/edit-identity.component';
|
import { EditIdentityComponent } from './components/edit-identity/edit-identity.component';
|
||||||
import { HomeComponent as EditIdentityHomeComponent } from './components/edit-identity/home/home.component';
|
import { HomeComponent as EditIdentityHomeComponent } from './components/edit-identity/home/home.component';
|
||||||
import { KeysComponent as EditIdentityKeysComponent } from './components/edit-identity/keys/keys.component';
|
import { KeysComponent as EditIdentityKeysComponent } from './components/edit-identity/keys/keys.component';
|
||||||
|
import { NcryptsecComponent as EditIdentityNcryptsecComponent } from './components/edit-identity/ncryptsec/ncryptsec.component';
|
||||||
import { PermissionsComponent as EditIdentityPermissionsComponent } from './components/edit-identity/permissions/permissions.component';
|
import { PermissionsComponent as EditIdentityPermissionsComponent } from './components/edit-identity/permissions/permissions.component';
|
||||||
import { RelaysComponent as EditIdentityRelaysComponent } from './components/edit-identity/relays/relays.component';
|
import { RelaysComponent as EditIdentityRelaysComponent } from './components/edit-identity/relays/relays.component';
|
||||||
import { VaultImportComponent } from './components/vault-import/vault-import.component';
|
import { VaultImportComponent } from './components/vault-import/vault-import.component';
|
||||||
@@ -112,6 +113,10 @@ export const routes: Routes = [
|
|||||||
path: 'keys',
|
path: 'keys',
|
||||||
component: EditIdentityKeysComponent,
|
component: EditIdentityKeysComponent,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'ncryptsec',
|
||||||
|
component: EditIdentityNcryptsecComponent,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'permissions',
|
path: 'permissions',
|
||||||
component: EditIdentityPermissionsComponent,
|
component: EditIdentityPermissionsComponent,
|
||||||
|
|||||||
@@ -136,6 +136,12 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<span class="sam-mt-2">Encrypted Key (NIP-49)</span>
|
||||||
|
|
||||||
|
<button class="btn btn-primary sam-mt-h" (click)="navigateToNcryptsec()">
|
||||||
|
Get ncryptsec
|
||||||
|
</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
<lib-toast #toast [bottom]="16"></lib-toast>
|
<lib-toast #toast [bottom]="16"></lib-toast>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component, inject, OnInit } from '@angular/core';
|
import { Component, inject, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import {
|
import {
|
||||||
IconButtonComponent,
|
IconButtonComponent,
|
||||||
NavComponent,
|
NavComponent,
|
||||||
@@ -29,6 +29,7 @@ export class KeysComponent extends NavComponent implements OnInit {
|
|||||||
|
|
||||||
readonly #activatedRoute = inject(ActivatedRoute);
|
readonly #activatedRoute = inject(ActivatedRoute);
|
||||||
readonly #storage = inject(StorageService);
|
readonly #storage = inject(StorageService);
|
||||||
|
readonly #router = inject(Router);
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
const identityId = this.#activatedRoute.parent?.snapshot.params['id'];
|
const identityId = this.#activatedRoute.parent?.snapshot.params['id'];
|
||||||
@@ -51,6 +52,11 @@ export class KeysComponent extends NavComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navigateToNcryptsec() {
|
||||||
|
if (!this.identity) return;
|
||||||
|
this.#router.navigateByUrl(`/edit-identity/${this.identity.id}/ncryptsec`);
|
||||||
|
}
|
||||||
|
|
||||||
async #initialize(identityId: string) {
|
async #initialize(identityId: string) {
|
||||||
const identity = this.#storage
|
const identity = this.#storage
|
||||||
.getBrowserSessionHandler()
|
.getBrowserSessionHandler()
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<div class="header-pane">
|
||||||
|
<lib-icon-button
|
||||||
|
icon="chevron-left"
|
||||||
|
(click)="navigateBack()"
|
||||||
|
></lib-icon-button>
|
||||||
|
<span>Get ncryptsec</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="description">
|
||||||
|
Enter a password to encrypt your private key. The resulting ncryptsec can be
|
||||||
|
used to securely backup or transfer your key.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- PASSWORD INPUT -->
|
||||||
|
<div class="password-section">
|
||||||
|
<label for="ncryptsecPasswordInput">Password</label>
|
||||||
|
<div class="input-group sam-mt-h">
|
||||||
|
<input
|
||||||
|
id="ncryptsecPasswordInput"
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Enter encryption password"
|
||||||
|
[(ngModel)]="ncryptsecPassword"
|
||||||
|
[disabled]="isGenerating"
|
||||||
|
(keyup.enter)="generateNcryptsec()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn-primary generate-btn"
|
||||||
|
type="button"
|
||||||
|
(click)="generateNcryptsec()"
|
||||||
|
[disabled]="!ncryptsecPassword || isGenerating"
|
||||||
|
>
|
||||||
|
@if (isGenerating) {
|
||||||
|
<span class="spinner-border spinner-border-sm" role="status"></span>
|
||||||
|
Generating...
|
||||||
|
} @else {
|
||||||
|
Generate ncryptsec
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- NCRYPTSEC OUTPUT -->
|
||||||
|
@if (ncryptsec) {
|
||||||
|
<div class="result-section">
|
||||||
|
<!-- QR Code -->
|
||||||
|
<div class="qr-container">
|
||||||
|
<img [src]="ncryptsecQr" alt="ncryptsec QR code" class="qr-code" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ncryptsec text -->
|
||||||
|
<div class="ncryptsec-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control ncryptsec-output"
|
||||||
|
[value]="ncryptsec"
|
||||||
|
readonly
|
||||||
|
(click)="copyToClipboard(ncryptsec); toast.show('Copied to clipboard')"
|
||||||
|
title="Click to copy"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
(click)="copyToClipboard(ncryptsec); toast.show('Copied to clipboard')"
|
||||||
|
>
|
||||||
|
<i class="bi bi-copy"></i> Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="hint">Tap the text or button to copy to clipboard</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<lib-toast #toast [bottom]="16"></lib-toast>
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
:host {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-left: var(--size);
|
||||||
|
padding-right: var(--size);
|
||||||
|
|
||||||
|
.header-pane {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
column-gap: var(--size-h);
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: var(--size);
|
||||||
|
background-color: var(--background);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: var(--size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-section {
|
||||||
|
margin-bottom: var(--size);
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: var(--size-q);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: var(--size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--size);
|
||||||
|
margin-top: var(--size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-container {
|
||||||
|
background: white;
|
||||||
|
padding: var(--size);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code {
|
||||||
|
width: 250px;
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ncryptsec-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--size-h);
|
||||||
|
|
||||||
|
.ncryptsec-output {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
word-break: break-all;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bs-gray-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { Component, inject, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import {
|
||||||
|
IconButtonComponent,
|
||||||
|
NavComponent,
|
||||||
|
NostrHelper,
|
||||||
|
StorageService,
|
||||||
|
ToastComponent,
|
||||||
|
} from '@common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import * as QRCode from 'qrcode';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-ncryptsec',
|
||||||
|
imports: [IconButtonComponent, FormsModule, ToastComponent],
|
||||||
|
templateUrl: './ncryptsec.component.html',
|
||||||
|
styleUrl: './ncryptsec.component.scss',
|
||||||
|
})
|
||||||
|
export class NcryptsecComponent extends NavComponent implements OnInit {
|
||||||
|
privkeyHex = '';
|
||||||
|
ncryptsecPassword = '';
|
||||||
|
ncryptsec = '';
|
||||||
|
ncryptsecQr = '';
|
||||||
|
isGenerating = false;
|
||||||
|
|
||||||
|
readonly #activatedRoute = inject(ActivatedRoute);
|
||||||
|
readonly #storage = inject(StorageService);
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const identityId = this.#activatedRoute.parent?.snapshot.params['id'];
|
||||||
|
if (!identityId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#initialize(identityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateNcryptsec() {
|
||||||
|
if (!this.privkeyHex || !this.ncryptsecPassword) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isGenerating = true;
|
||||||
|
this.ncryptsec = '';
|
||||||
|
this.ncryptsecQr = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.ncryptsec = await NostrHelper.privkeyToNcryptsec(
|
||||||
|
this.privkeyHex,
|
||||||
|
this.ncryptsecPassword
|
||||||
|
);
|
||||||
|
|
||||||
|
// Generate QR code
|
||||||
|
this.ncryptsecQr = await QRCode.toDataURL(this.ncryptsec, {
|
||||||
|
width: 250,
|
||||||
|
margin: 2,
|
||||||
|
color: {
|
||||||
|
dark: '#000000',
|
||||||
|
light: '#ffffff',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to generate ncryptsec:', error);
|
||||||
|
} finally {
|
||||||
|
this.isGenerating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copyToClipboard(text: string) {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
#initialize(identityId: string) {
|
||||||
|
const identity = this.#storage
|
||||||
|
.getBrowserSessionHandler()
|
||||||
|
.browserSessionData?.identities.find((x) => x.id === identityId);
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.privkeyHex = identity.privkey;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import { bech32 } from '@scure/base';
|
import { bech32 } from '@scure/base';
|
||||||
import * as utils from '@noble/curves/abstract/utils';
|
import * as utils from '@noble/curves/abstract/utils';
|
||||||
import { getPublicKey } from 'nostr-tools';
|
import { getPublicKey } from 'nostr-tools';
|
||||||
|
import { encrypt as nip49Encrypt } from 'nostr-tools/nip49';
|
||||||
|
|
||||||
export interface NostrHexObject {
|
export interface NostrHexObject {
|
||||||
represents: string;
|
represents: string;
|
||||||
@@ -125,4 +126,21 @@ export class NostrHelper {
|
|||||||
hex: utils.bytesToHex(data),
|
hex: utils.bytesToHex(data),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts a private key (hex) with a password using NIP-49.
|
||||||
|
* Returns an ncryptsec bech32 string.
|
||||||
|
* @param privkeyHex - The private key in hex format
|
||||||
|
* @param password - The password to encrypt with
|
||||||
|
* @param logN - Optional log2(N) parameter for scrypt (default: 16)
|
||||||
|
* @returns Promise<string> - The ncryptsec bech32 encoded encrypted key
|
||||||
|
*/
|
||||||
|
static async privkeyToNcryptsec(
|
||||||
|
privkeyHex: string,
|
||||||
|
password: string,
|
||||||
|
logN = 16
|
||||||
|
): Promise<string> {
|
||||||
|
const privkeyBytes = utils.hexToBytes(privkeyHex);
|
||||||
|
return nip49Encrypt(privkeyBytes, password, logN);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Plebeian Signer",
|
"name": "Plebeian Signer",
|
||||||
"description": "Nostr Identity Manager & Signer",
|
"description": "Nostr Identity Manager & Signer",
|
||||||
"version": "1.1.2",
|
"version": "1.1.3",
|
||||||
"homepage_url": "https://github.com/PlebeianApp/plebeian-signer",
|
"homepage_url": "https://github.com/PlebeianApp/plebeian-signer",
|
||||||
"options_page": "options.html",
|
"options_page": "options.html",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { NewIdentityComponent } from './components/new-identity/new-identity.com
|
|||||||
import { EditIdentityComponent } from './components/edit-identity/edit-identity.component';
|
import { EditIdentityComponent } from './components/edit-identity/edit-identity.component';
|
||||||
import { HomeComponent as EditIdentityHomeComponent } from './components/edit-identity/home/home.component';
|
import { HomeComponent as EditIdentityHomeComponent } from './components/edit-identity/home/home.component';
|
||||||
import { KeysComponent as EditIdentityKeysComponent } from './components/edit-identity/keys/keys.component';
|
import { KeysComponent as EditIdentityKeysComponent } from './components/edit-identity/keys/keys.component';
|
||||||
|
import { NcryptsecComponent as EditIdentityNcryptsecComponent } from './components/edit-identity/ncryptsec/ncryptsec.component';
|
||||||
import { PermissionsComponent as EditIdentityPermissionsComponent } from './components/edit-identity/permissions/permissions.component';
|
import { PermissionsComponent as EditIdentityPermissionsComponent } from './components/edit-identity/permissions/permissions.component';
|
||||||
import { RelaysComponent as EditIdentityRelaysComponent } from './components/edit-identity/relays/relays.component';
|
import { RelaysComponent as EditIdentityRelaysComponent } from './components/edit-identity/relays/relays.component';
|
||||||
import { WelcomeComponent } from './components/welcome/welcome.component';
|
import { WelcomeComponent } from './components/welcome/welcome.component';
|
||||||
@@ -112,6 +113,10 @@ export const routes: Routes = [
|
|||||||
path: 'keys',
|
path: 'keys',
|
||||||
component: EditIdentityKeysComponent,
|
component: EditIdentityKeysComponent,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'ncryptsec',
|
||||||
|
component: EditIdentityNcryptsecComponent,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'permissions',
|
path: 'permissions',
|
||||||
component: EditIdentityPermissionsComponent,
|
component: EditIdentityPermissionsComponent,
|
||||||
|
|||||||
@@ -136,6 +136,12 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<span class="sam-mt-2">Encrypted Key (NIP-49)</span>
|
||||||
|
|
||||||
|
<button class="btn btn-primary sam-mt-h" (click)="navigateToNcryptsec()">
|
||||||
|
Get ncryptsec
|
||||||
|
</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
<lib-toast #toast [bottom]="16"></lib-toast>
|
<lib-toast #toast [bottom]="16"></lib-toast>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, inject, OnInit } from '@angular/core';
|
import { Component, inject, OnInit } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import {
|
import {
|
||||||
IconButtonComponent,
|
IconButtonComponent,
|
||||||
NavComponent,
|
NavComponent,
|
||||||
@@ -29,6 +29,7 @@ export class KeysComponent extends NavComponent implements OnInit {
|
|||||||
|
|
||||||
readonly #activatedRoute = inject(ActivatedRoute);
|
readonly #activatedRoute = inject(ActivatedRoute);
|
||||||
readonly #storage = inject(StorageService);
|
readonly #storage = inject(StorageService);
|
||||||
|
readonly #router = inject(Router);
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
const identityId = this.#activatedRoute.parent?.snapshot.params['id'];
|
const identityId = this.#activatedRoute.parent?.snapshot.params['id'];
|
||||||
@@ -51,6 +52,11 @@ export class KeysComponent extends NavComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navigateToNcryptsec() {
|
||||||
|
if (!this.identity) return;
|
||||||
|
this.#router.navigateByUrl(`/edit-identity/${this.identity.id}/ncryptsec`);
|
||||||
|
}
|
||||||
|
|
||||||
async #initialize(identityId: string) {
|
async #initialize(identityId: string) {
|
||||||
const identity = this.#storage
|
const identity = this.#storage
|
||||||
.getBrowserSessionHandler()
|
.getBrowserSessionHandler()
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<div class="header-pane">
|
||||||
|
<lib-icon-button
|
||||||
|
icon="chevron-left"
|
||||||
|
(click)="navigateBack()"
|
||||||
|
></lib-icon-button>
|
||||||
|
<span>Get ncryptsec</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="description">
|
||||||
|
Enter a password to encrypt your private key. The resulting ncryptsec can be
|
||||||
|
used to securely backup or transfer your key.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- PASSWORD INPUT -->
|
||||||
|
<div class="password-section">
|
||||||
|
<label for="ncryptsecPasswordInput">Password</label>
|
||||||
|
<div class="input-group sam-mt-h">
|
||||||
|
<input
|
||||||
|
id="ncryptsecPasswordInput"
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Enter encryption password"
|
||||||
|
[(ngModel)]="ncryptsecPassword"
|
||||||
|
[disabled]="isGenerating"
|
||||||
|
(keyup.enter)="generateNcryptsec()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn-primary generate-btn"
|
||||||
|
type="button"
|
||||||
|
(click)="generateNcryptsec()"
|
||||||
|
[disabled]="!ncryptsecPassword || isGenerating"
|
||||||
|
>
|
||||||
|
@if (isGenerating) {
|
||||||
|
<span class="spinner-border spinner-border-sm" role="status"></span>
|
||||||
|
Generating...
|
||||||
|
} @else {
|
||||||
|
Generate ncryptsec
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- NCRYPTSEC OUTPUT -->
|
||||||
|
@if (ncryptsec) {
|
||||||
|
<div class="result-section">
|
||||||
|
<!-- QR Code -->
|
||||||
|
<div class="qr-container">
|
||||||
|
<img [src]="ncryptsecQr" alt="ncryptsec QR code" class="qr-code" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ncryptsec text -->
|
||||||
|
<div class="ncryptsec-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control ncryptsec-output"
|
||||||
|
[value]="ncryptsec"
|
||||||
|
readonly
|
||||||
|
(click)="copyToClipboard(ncryptsec); toast.show('Copied to clipboard')"
|
||||||
|
title="Click to copy"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
(click)="copyToClipboard(ncryptsec); toast.show('Copied to clipboard')"
|
||||||
|
>
|
||||||
|
<i class="bi bi-copy"></i> Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="hint">Tap the text or button to copy to clipboard</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<lib-toast #toast [bottom]="16"></lib-toast>
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
:host {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-left: var(--size);
|
||||||
|
padding-right: var(--size);
|
||||||
|
|
||||||
|
.header-pane {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
column-gap: var(--size-h);
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: var(--size);
|
||||||
|
background-color: var(--background);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: var(--size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-section {
|
||||||
|
margin-bottom: var(--size);
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: var(--size-q);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: var(--size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--size);
|
||||||
|
margin-top: var(--size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-container {
|
||||||
|
background: white;
|
||||||
|
padding: var(--size);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code {
|
||||||
|
width: 250px;
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ncryptsec-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--size-h);
|
||||||
|
|
||||||
|
.ncryptsec-output {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
word-break: break-all;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bs-gray-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { Component, inject, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import {
|
||||||
|
IconButtonComponent,
|
||||||
|
NavComponent,
|
||||||
|
NostrHelper,
|
||||||
|
StorageService,
|
||||||
|
ToastComponent,
|
||||||
|
} from '@common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import * as QRCode from 'qrcode';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-ncryptsec',
|
||||||
|
imports: [IconButtonComponent, FormsModule, ToastComponent],
|
||||||
|
templateUrl: './ncryptsec.component.html',
|
||||||
|
styleUrl: './ncryptsec.component.scss',
|
||||||
|
})
|
||||||
|
export class NcryptsecComponent extends NavComponent implements OnInit {
|
||||||
|
privkeyHex = '';
|
||||||
|
ncryptsecPassword = '';
|
||||||
|
ncryptsec = '';
|
||||||
|
ncryptsecQr = '';
|
||||||
|
isGenerating = false;
|
||||||
|
|
||||||
|
readonly #activatedRoute = inject(ActivatedRoute);
|
||||||
|
readonly #storage = inject(StorageService);
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const identityId = this.#activatedRoute.parent?.snapshot.params['id'];
|
||||||
|
if (!identityId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#initialize(identityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateNcryptsec() {
|
||||||
|
if (!this.privkeyHex || !this.ncryptsecPassword) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isGenerating = true;
|
||||||
|
this.ncryptsec = '';
|
||||||
|
this.ncryptsecQr = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.ncryptsec = await NostrHelper.privkeyToNcryptsec(
|
||||||
|
this.privkeyHex,
|
||||||
|
this.ncryptsecPassword
|
||||||
|
);
|
||||||
|
|
||||||
|
// Generate QR code
|
||||||
|
this.ncryptsecQr = await QRCode.toDataURL(this.ncryptsec, {
|
||||||
|
width: 250,
|
||||||
|
margin: 2,
|
||||||
|
color: {
|
||||||
|
dark: '#000000',
|
||||||
|
light: '#ffffff',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to generate ncryptsec:', error);
|
||||||
|
} finally {
|
||||||
|
this.isGenerating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copyToClipboard(text: string) {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
#initialize(identityId: string) {
|
||||||
|
const identity = this.#storage
|
||||||
|
.getBrowserSessionHandler()
|
||||||
|
.browserSessionData?.identities.find((x) => x.id === identityId);
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.privkeyHex = identity.privkey;
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
BIN
releases/plebeian-signer-chrome-v1.1.3.tar.gz
Normal file
BIN
releases/plebeian-signer-chrome-v1.1.3.tar.gz
Normal file
Binary file not shown.
Binary file not shown.
BIN
releases/plebeian-signer-firefox-v1.1.3.tar.gz
Normal file
BIN
releases/plebeian-signer-firefox-v1.1.3.tar.gz
Normal file
Binary file not shown.
Reference in New Issue
Block a user