diff --git a/package.json b/package.json
index 9f14b3b..1dc4c7c 100644
--- a/package.json
+++ b/package.json
@@ -1,12 +1,12 @@
{
"name": "plebeian-signer",
- "version": "v1.1.2",
+ "version": "v1.1.3",
"custom": {
"chrome": {
- "version": "v1.1.2"
+ "version": "v1.1.3"
},
"firefox": {
- "version": "v1.1.2"
+ "version": "v1.1.3"
}
},
"scripts": {
diff --git a/projects/chrome/public/manifest.json b/projects/chrome/public/manifest.json
index 8b17ff3..ee98a6f 100644
--- a/projects/chrome/public/manifest.json
+++ b/projects/chrome/public/manifest.json
@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Plebeian Signer - Nostr Identity Manager & Signer",
"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",
"options_page": "options.html",
"permissions": [
diff --git a/projects/chrome/src/app/app.routes.ts b/projects/chrome/src/app/app.routes.ts
index e0be536..37c833a 100644
--- a/projects/chrome/src/app/app.routes.ts
+++ b/projects/chrome/src/app/app.routes.ts
@@ -17,6 +17,7 @@ import { NewIdentityComponent } from './components/new-identity/new-identity.com
import { EditIdentityComponent } from './components/edit-identity/edit-identity.component';
import { HomeComponent as EditIdentityHomeComponent } from './components/edit-identity/home/home.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 { RelaysComponent as EditIdentityRelaysComponent } from './components/edit-identity/relays/relays.component';
import { VaultImportComponent } from './components/vault-import/vault-import.component';
@@ -112,6 +113,10 @@ export const routes: Routes = [
path: 'keys',
component: EditIdentityKeysComponent,
},
+ {
+ path: 'ncryptsec',
+ component: EditIdentityNcryptsecComponent,
+ },
{
path: 'permissions',
component: EditIdentityPermissionsComponent,
diff --git a/projects/chrome/src/app/components/edit-identity/keys/keys.component.html b/projects/chrome/src/app/components/edit-identity/keys/keys.component.html
index 5c52ad0..7096fbe 100644
--- a/projects/chrome/src/app/components/edit-identity/keys/keys.component.html
+++ b/projects/chrome/src/app/components/edit-identity/keys/keys.component.html
@@ -136,6 +136,12 @@
+
+Encrypted Key (NIP-49)
+
+
}
diff --git a/projects/chrome/src/app/components/edit-identity/keys/keys.component.ts b/projects/chrome/src/app/components/edit-identity/keys/keys.component.ts
index c7ea1d8..caf5431 100644
--- a/projects/chrome/src/app/components/edit-identity/keys/keys.component.ts
+++ b/projects/chrome/src/app/components/edit-identity/keys/keys.component.ts
@@ -1,5 +1,5 @@
import { Component, inject, OnInit } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
+import { ActivatedRoute, Router } from '@angular/router';
import {
IconButtonComponent,
NavComponent,
@@ -29,6 +29,7 @@ export class KeysComponent extends NavComponent implements OnInit {
readonly #activatedRoute = inject(ActivatedRoute);
readonly #storage = inject(StorageService);
+ readonly #router = inject(Router);
ngOnInit(): void {
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) {
const identity = this.#storage
.getBrowserSessionHandler()
diff --git a/projects/chrome/src/app/components/edit-identity/ncryptsec/ncryptsec.component.html b/projects/chrome/src/app/components/edit-identity/ncryptsec/ncryptsec.component.html
new file mode 100644
index 0000000..afc41fe
--- /dev/null
+++ b/projects/chrome/src/app/components/edit-identity/ncryptsec/ncryptsec.component.html
@@ -0,0 +1,75 @@
+
+
+
+ Enter a password to encrypt your private key. The resulting ncryptsec can be
+ used to securely backup or transfer your key.
+
+
+
+
+
+
+
+
+@if (ncryptsec) {
+
+
+
+
![ncryptsec QR code]()
+
+
+
+
+
+
+
+
+
Tap the text or button to copy to clipboard
+
+}
+
+
diff --git a/projects/chrome/src/app/components/edit-identity/ncryptsec/ncryptsec.component.scss b/projects/chrome/src/app/components/edit-identity/ncryptsec/ncryptsec.component.scss
new file mode 100644
index 0000000..b0d20f0
--- /dev/null
+++ b/projects/chrome/src/app/components/edit-identity/ncryptsec/ncryptsec.component.scss
@@ -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;
+}
diff --git a/projects/chrome/src/app/components/edit-identity/ncryptsec/ncryptsec.component.ts b/projects/chrome/src/app/components/edit-identity/ncryptsec/ncryptsec.component.ts
new file mode 100644
index 0000000..87ce6f3
--- /dev/null
+++ b/projects/chrome/src/app/components/edit-identity/ncryptsec/ncryptsec.component.ts
@@ -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;
+ }
+}
diff --git a/projects/common/src/lib/helpers/nostr-helper.ts b/projects/common/src/lib/helpers/nostr-helper.ts
index 536ae93..09ea133 100644
--- a/projects/common/src/lib/helpers/nostr-helper.ts
+++ b/projects/common/src/lib/helpers/nostr-helper.ts
@@ -2,6 +2,7 @@
import { bech32 } from '@scure/base';
import * as utils from '@noble/curves/abstract/utils';
import { getPublicKey } from 'nostr-tools';
+import { encrypt as nip49Encrypt } from 'nostr-tools/nip49';
export interface NostrHexObject {
represents: string;
@@ -125,4 +126,21 @@ export class NostrHelper {
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 - The ncryptsec bech32 encoded encrypted key
+ */
+ static async privkeyToNcryptsec(
+ privkeyHex: string,
+ password: string,
+ logN = 16
+ ): Promise {
+ const privkeyBytes = utils.hexToBytes(privkeyHex);
+ return nip49Encrypt(privkeyBytes, password, logN);
+ }
}
diff --git a/projects/firefox/public/manifest.json b/projects/firefox/public/manifest.json
index 7e6d9fe..ba3dfeb 100644
--- a/projects/firefox/public/manifest.json
+++ b/projects/firefox/public/manifest.json
@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Plebeian Signer",
"description": "Nostr Identity Manager & Signer",
- "version": "1.1.2",
+ "version": "1.1.3",
"homepage_url": "https://github.com/PlebeianApp/plebeian-signer",
"options_page": "options.html",
"permissions": [
diff --git a/projects/firefox/src/app/app.routes.ts b/projects/firefox/src/app/app.routes.ts
index 5c4eae5..3859a3c 100644
--- a/projects/firefox/src/app/app.routes.ts
+++ b/projects/firefox/src/app/app.routes.ts
@@ -14,6 +14,7 @@ import { NewIdentityComponent } from './components/new-identity/new-identity.com
import { EditIdentityComponent } from './components/edit-identity/edit-identity.component';
import { HomeComponent as EditIdentityHomeComponent } from './components/edit-identity/home/home.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 { RelaysComponent as EditIdentityRelaysComponent } from './components/edit-identity/relays/relays.component';
import { WelcomeComponent } from './components/welcome/welcome.component';
@@ -112,6 +113,10 @@ export const routes: Routes = [
path: 'keys',
component: EditIdentityKeysComponent,
},
+ {
+ path: 'ncryptsec',
+ component: EditIdentityNcryptsecComponent,
+ },
{
path: 'permissions',
component: EditIdentityPermissionsComponent,
diff --git a/projects/firefox/src/app/components/edit-identity/keys/keys.component.html b/projects/firefox/src/app/components/edit-identity/keys/keys.component.html
index 5c52ad0..7096fbe 100644
--- a/projects/firefox/src/app/components/edit-identity/keys/keys.component.html
+++ b/projects/firefox/src/app/components/edit-identity/keys/keys.component.html
@@ -136,6 +136,12 @@
+
+Encrypted Key (NIP-49)
+
+
}
diff --git a/projects/firefox/src/app/components/edit-identity/keys/keys.component.ts b/projects/firefox/src/app/components/edit-identity/keys/keys.component.ts
index b9a9699..3756b7c 100644
--- a/projects/firefox/src/app/components/edit-identity/keys/keys.component.ts
+++ b/projects/firefox/src/app/components/edit-identity/keys/keys.component.ts
@@ -1,6 +1,6 @@
import { Component, inject, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
-import { ActivatedRoute } from '@angular/router';
+import { ActivatedRoute, Router } from '@angular/router';
import {
IconButtonComponent,
NavComponent,
@@ -29,6 +29,7 @@ export class KeysComponent extends NavComponent implements OnInit {
readonly #activatedRoute = inject(ActivatedRoute);
readonly #storage = inject(StorageService);
+ readonly #router = inject(Router);
ngOnInit(): void {
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) {
const identity = this.#storage
.getBrowserSessionHandler()
diff --git a/projects/firefox/src/app/components/edit-identity/ncryptsec/ncryptsec.component.html b/projects/firefox/src/app/components/edit-identity/ncryptsec/ncryptsec.component.html
new file mode 100644
index 0000000..afc41fe
--- /dev/null
+++ b/projects/firefox/src/app/components/edit-identity/ncryptsec/ncryptsec.component.html
@@ -0,0 +1,75 @@
+
+
+
+ Enter a password to encrypt your private key. The resulting ncryptsec can be
+ used to securely backup or transfer your key.
+
+
+
+
+
+
+
+
+@if (ncryptsec) {
+
+
+
+
![ncryptsec QR code]()
+
+
+
+
+
+
+
+
+
Tap the text or button to copy to clipboard
+
+}
+
+
diff --git a/projects/firefox/src/app/components/edit-identity/ncryptsec/ncryptsec.component.scss b/projects/firefox/src/app/components/edit-identity/ncryptsec/ncryptsec.component.scss
new file mode 100644
index 0000000..b0d20f0
--- /dev/null
+++ b/projects/firefox/src/app/components/edit-identity/ncryptsec/ncryptsec.component.scss
@@ -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;
+}
diff --git a/projects/firefox/src/app/components/edit-identity/ncryptsec/ncryptsec.component.ts b/projects/firefox/src/app/components/edit-identity/ncryptsec/ncryptsec.component.ts
new file mode 100644
index 0000000..87ce6f3
--- /dev/null
+++ b/projects/firefox/src/app/components/edit-identity/ncryptsec/ncryptsec.component.ts
@@ -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;
+ }
+}
diff --git a/releases/plebeian-signer-chrome-v1.1.2.zip b/releases/plebeian-signer-chrome-v1.1.2.zip
deleted file mode 100644
index 18291a8..0000000
Binary files a/releases/plebeian-signer-chrome-v1.1.2.zip and /dev/null differ
diff --git a/releases/plebeian-signer-chrome-v1.1.3.tar.gz b/releases/plebeian-signer-chrome-v1.1.3.tar.gz
new file mode 100644
index 0000000..390546a
Binary files /dev/null and b/releases/plebeian-signer-chrome-v1.1.3.tar.gz differ
diff --git a/releases/plebeian-signer-firefox-v1.1.2.zip b/releases/plebeian-signer-firefox-v1.1.2.zip
deleted file mode 100644
index 7b8b379..0000000
Binary files a/releases/plebeian-signer-firefox-v1.1.2.zip and /dev/null differ
diff --git a/releases/plebeian-signer-firefox-v1.1.3.tar.gz b/releases/plebeian-signer-firefox-v1.1.3.tar.gz
new file mode 100644
index 0000000..80f05e4
Binary files /dev/null and b/releases/plebeian-signer-firefox-v1.1.3.tar.gz differ