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 @@ +
+ + Get ncryptsec +
+ +

+ 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 @@ +
+ + Get ncryptsec +
+ +

+ 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