Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5183a4fc0a | ||
|
|
a2d0a9bd32 | ||
|
|
5cf0fed4ed | ||
|
|
4a2bc4fe72 | ||
| a2e47d8612 | |||
| 2074c409f0 |
24
LICENSE
Normal file
@@ -0,0 +1,24 @@
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <http://unlicense.org/>
|
||||
68
PRIVACY_POLICY.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Privacy Policy
|
||||
|
||||
**Plebeian Signer** is a browser extension for managing Nostr identities and signing events. This privacy policy explains how the extension handles your data.
|
||||
|
||||
## Data Collection
|
||||
|
||||
**Plebeian Signer does not collect, store, or transmit any user data to external servers.**
|
||||
|
||||
All data remains on your device under your control.
|
||||
|
||||
## Data Storage
|
||||
|
||||
The extension stores the following data locally in your browser:
|
||||
|
||||
- **Encrypted vault**: Your Nostr private keys, encrypted with your password using Argon2id + AES-256-GCM
|
||||
- **Identity metadata**: Display names, profile information you configure
|
||||
- **Permissions**: Your allow/deny decisions for websites
|
||||
- **Cashu wallet data**: Mint connections and ecash tokens you store
|
||||
- **Preferences**: Extension settings (sync mode, reckless mode, etc.)
|
||||
|
||||
This data is stored using your browser's built-in storage APIs and never leaves your device unless you enable browser sync (in which case it syncs through your browser's own sync service, not ours).
|
||||
|
||||
## External Connections
|
||||
|
||||
The extension only makes external network requests in the following cases:
|
||||
|
||||
1. **Cashu mints**: When you explicitly add a Cashu mint and perform wallet operations (deposit, send, receive), the extension connects to that mint's URL. You choose which mints to connect to.
|
||||
|
||||
2. **No other external connections**: The extension does not connect to any analytics services, tracking pixels, telemetry endpoints, or any servers operated by the developers.
|
||||
|
||||
## Third-Party Services
|
||||
|
||||
Plebeian Signer does not integrate with any third-party services. The only external services involved are:
|
||||
|
||||
- **Cashu mints**: User-configured ecash mints for wallet functionality
|
||||
- **Browser sync** (optional): Your browser's native sync service if you enable vault syncing
|
||||
|
||||
## Data Sharing
|
||||
|
||||
We do not share any data because we do not have access to any data. Your private keys and all extension data remain encrypted on your device.
|
||||
|
||||
## Security
|
||||
|
||||
- Private keys are encrypted at rest using Argon2id key derivation and AES-256-GCM encryption
|
||||
- Keys are never exposed to websites — only signatures are provided
|
||||
- The vault locks automatically and requires your password to unlock
|
||||
|
||||
## Your Rights
|
||||
|
||||
Since all data is stored locally on your device:
|
||||
|
||||
- **Access**: View your data anytime in the extension
|
||||
- **Delete**: Uninstall the extension or clear browser data to remove all stored data
|
||||
- **Export**: Use the extension's export features to backup your data
|
||||
|
||||
## Changes to This Policy
|
||||
|
||||
Any changes to this privacy policy will be reflected in the extension's repository and release notes.
|
||||
|
||||
## Contact
|
||||
|
||||
For questions about this privacy policy, please open an issue at the project repository.
|
||||
|
||||
---
|
||||
|
||||
**Last updated**: January 2026
|
||||
|
||||
**Extension**: Plebeian Signer v1.1.5
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "plebeian-signer",
|
||||
"version": "v1.1.2",
|
||||
"version": "1.1.6",
|
||||
"custom": {
|
||||
"chrome": {
|
||||
"version": "v1.1.2"
|
||||
"version": "v1.1.6"
|
||||
},
|
||||
"firefox": {
|
||||
"version": "v1.1.2"
|
||||
"version": "v1.1.6"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
@@ -74,5 +74,6 @@
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "~5.6.2",
|
||||
"typescript-eslint": "8.18.0"
|
||||
}
|
||||
},
|
||||
"license": "Unlicense"
|
||||
}
|
||||
|
||||
@@ -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.5",
|
||||
"homepage_url": "https://github.com/PlebeianApp/plebeian-signer",
|
||||
"options_page": "options.html",
|
||||
"permissions": [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -136,6 +136,12 @@
|
||||
</button>
|
||||
</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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<div class="header-pane">
|
||||
<lib-icon-button
|
||||
icon="chevron-left"
|
||||
(click)="navigateBack()"
|
||||
></lib-icon-button>
|
||||
<span>Get ncryptsec</span>
|
||||
</div>
|
||||
|
||||
<!-- QR Code (shown after generation) -->
|
||||
@if (ncryptsec) {
|
||||
<div class="qr-container">
|
||||
<button
|
||||
type="button"
|
||||
class="qr-button"
|
||||
title="Copy to clipboard"
|
||||
(click)="copyToClipboard(ncryptsec); toast.show('Copied to clipboard')"
|
||||
>
|
||||
<img [src]="ncryptsecQr" alt="ncryptsec QR code" class="qr-code" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- PASSWORD INPUT -->
|
||||
<div class="password-section">
|
||||
<label for="ncryptsecPasswordInput">Password</label>
|
||||
<div class="input-group sam-mt-h">
|
||||
<input
|
||||
#passwordInput
|
||||
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>
|
||||
|
||||
|
||||
<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>
|
||||
|
||||
<lib-toast #toast [bottom]="16"></lib-toast>
|
||||
@@ -0,0 +1,70 @@
|
||||
: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);
|
||||
}
|
||||
|
||||
.qr-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: var(--size);
|
||||
}
|
||||
|
||||
.qr-button {
|
||||
background: white;
|
||||
padding: var(--size);
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
inject,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
} 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, AfterViewInit
|
||||
{
|
||||
@ViewChild('passwordInput') passwordInput!: ElementRef<HTMLInputElement>;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.passwordInput.nativeElement.focus();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -40,10 +40,13 @@ export interface UnlockResponseMessage {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const debug = function (message: any) {
|
||||
const dateString = new Date().toISOString();
|
||||
console.log(`[Plebeian Signer - ${dateString}]: ${JSON.stringify(message)}`);
|
||||
};
|
||||
// Debug logging disabled - uncomment for development
|
||||
// export const debug = function (message: any) {
|
||||
// const dateString = new Date().toISOString();
|
||||
// console.log(`[Plebeian Signer - ${dateString}]: ${JSON.stringify(message)}`);
|
||||
// };
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
|
||||
export const debug = function (_message: any) {};
|
||||
|
||||
export type PromptResponse =
|
||||
| 'reject'
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
backgroundLogNip07Action,
|
||||
backgroundLogPermissionStored,
|
||||
NostrHelper,
|
||||
NwcClient,
|
||||
NwcConnection_DECRYPTED,
|
||||
@@ -441,7 +439,6 @@ async function processNip07Request(req: BackgroundRequestMessage): Promise<any>
|
||||
policy,
|
||||
req.params?.kind
|
||||
);
|
||||
await backgroundLogPermissionStored(req.host, req.method, policy, req.params?.kind);
|
||||
} else if (response === 'approve-all') {
|
||||
// P2: Store permission for ALL kinds/uses of this method from this host
|
||||
await storePermission(
|
||||
@@ -452,8 +449,6 @@ async function processNip07Request(req: BackgroundRequestMessage): Promise<any>
|
||||
'allow',
|
||||
undefined // undefined kind = allow all kinds for signEvent
|
||||
);
|
||||
await backgroundLogPermissionStored(req.host, req.method, 'allow', undefined);
|
||||
debug(`Stored approve-all permission for ${req.method} from ${req.host}`);
|
||||
} else if (response === 'reject-all') {
|
||||
// P2: Store deny permission for ALL uses of this method from this host
|
||||
await storePermission(
|
||||
@@ -464,15 +459,9 @@ async function processNip07Request(req: BackgroundRequestMessage): Promise<any>
|
||||
'deny',
|
||||
undefined
|
||||
);
|
||||
await backgroundLogPermissionStored(req.host, req.method, 'deny', undefined);
|
||||
debug(`Stored reject-all permission for ${req.method} from ${req.host}`);
|
||||
}
|
||||
|
||||
if (['reject', 'reject-once', 'reject-all'].includes(response)) {
|
||||
await backgroundLogNip07Action(req.method, req.host, false, false, {
|
||||
kind: req.params?.kind,
|
||||
peerPubkey: req.params?.peerPubkey,
|
||||
});
|
||||
throw new Error('Permission denied');
|
||||
}
|
||||
} else {
|
||||
@@ -481,71 +470,47 @@ async function processNip07Request(req: BackgroundRequestMessage): Promise<any>
|
||||
}
|
||||
|
||||
const relays: Relays = {};
|
||||
let result: any;
|
||||
|
||||
switch (req.method) {
|
||||
case 'getPublicKey':
|
||||
result = NostrHelper.pubkeyFromPrivkey(currentIdentity.privkey);
|
||||
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove);
|
||||
return result;
|
||||
return NostrHelper.pubkeyFromPrivkey(currentIdentity.privkey);
|
||||
|
||||
case 'signEvent':
|
||||
result = signEvent(req.params, currentIdentity.privkey);
|
||||
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
|
||||
kind: req.params?.kind,
|
||||
});
|
||||
return result;
|
||||
return signEvent(req.params, currentIdentity.privkey);
|
||||
|
||||
case 'getRelays':
|
||||
browserSessionData.relays.forEach((x) => {
|
||||
relays[x.url] = { read: x.read, write: x.write };
|
||||
});
|
||||
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove);
|
||||
return relays;
|
||||
|
||||
case 'nip04.encrypt':
|
||||
result = await nip04Encrypt(
|
||||
return await nip04Encrypt(
|
||||
currentIdentity.privkey,
|
||||
req.params.peerPubkey,
|
||||
req.params.plaintext
|
||||
);
|
||||
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
|
||||
peerPubkey: req.params.peerPubkey,
|
||||
});
|
||||
return result;
|
||||
|
||||
case 'nip44.encrypt':
|
||||
result = await nip44Encrypt(
|
||||
return await nip44Encrypt(
|
||||
currentIdentity.privkey,
|
||||
req.params.peerPubkey,
|
||||
req.params.plaintext
|
||||
);
|
||||
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
|
||||
peerPubkey: req.params.peerPubkey,
|
||||
});
|
||||
return result;
|
||||
|
||||
case 'nip04.decrypt':
|
||||
result = await nip04Decrypt(
|
||||
return await nip04Decrypt(
|
||||
currentIdentity.privkey,
|
||||
req.params.peerPubkey,
|
||||
req.params.ciphertext
|
||||
);
|
||||
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
|
||||
peerPubkey: req.params.peerPubkey,
|
||||
});
|
||||
return result;
|
||||
|
||||
case 'nip44.decrypt':
|
||||
result = await nip44Decrypt(
|
||||
return await nip44Decrypt(
|
||||
currentIdentity.privkey,
|
||||
req.params.peerPubkey,
|
||||
req.params.ciphertext
|
||||
);
|
||||
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
|
||||
peerPubkey: req.params.peerPubkey,
|
||||
});
|
||||
return result;
|
||||
|
||||
default:
|
||||
throw new Error(`Not supported request method '${req.method}'.`);
|
||||
@@ -625,7 +590,6 @@ async function processWeblnRequest(req: BackgroundRequestMessage): Promise<any>
|
||||
method,
|
||||
policy
|
||||
);
|
||||
await backgroundLogPermissionStored(req.host, method, policy);
|
||||
} else if (response === 'approve-all' && method !== 'webln.sendPayment' && method !== 'webln.keysend') {
|
||||
// P2: Store permission for all uses of this WebLN method
|
||||
await storePermission(
|
||||
@@ -635,8 +599,6 @@ async function processWeblnRequest(req: BackgroundRequestMessage): Promise<any>
|
||||
method,
|
||||
'allow'
|
||||
);
|
||||
await backgroundLogPermissionStored(req.host, method, 'allow');
|
||||
debug(`Stored approve-all permission for ${method} from ${req.host}`);
|
||||
}
|
||||
|
||||
if (['reject', 'reject-once', 'reject-all'].includes(response)) {
|
||||
|
||||
@@ -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<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,
|
||||
"name": "Plebeian Signer",
|
||||
"description": "Nostr Identity Manager & Signer",
|
||||
"version": "1.1.2",
|
||||
"version": "1.1.5",
|
||||
"homepage_url": "https://github.com/PlebeianApp/plebeian-signer",
|
||||
"options_page": "options.html",
|
||||
"permissions": [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -136,6 +136,12 @@
|
||||
</button>
|
||||
</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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<div class="header-pane">
|
||||
<lib-icon-button
|
||||
icon="chevron-left"
|
||||
(click)="navigateBack()"
|
||||
></lib-icon-button>
|
||||
<span>Get ncryptsec</span>
|
||||
</div>
|
||||
|
||||
<!-- QR Code (shown after generation) -->
|
||||
@if (ncryptsec) {
|
||||
<div class="qr-container">
|
||||
<button
|
||||
type="button"
|
||||
class="qr-button"
|
||||
title="Copy to clipboard"
|
||||
(click)="copyToClipboard(ncryptsec); toast.show('Copied to clipboard')"
|
||||
>
|
||||
<img [src]="ncryptsecQr" alt="ncryptsec QR code" class="qr-code" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- PASSWORD INPUT -->
|
||||
<div class="password-section">
|
||||
<label for="ncryptsecPasswordInput">Password</label>
|
||||
<div class="input-group sam-mt-h">
|
||||
<input
|
||||
#passwordInput
|
||||
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>
|
||||
|
||||
|
||||
<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>
|
||||
|
||||
<lib-toast #toast [bottom]="16"></lib-toast>
|
||||
@@ -0,0 +1,70 @@
|
||||
: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);
|
||||
}
|
||||
|
||||
.qr-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: var(--size);
|
||||
}
|
||||
|
||||
.qr-button {
|
||||
background: white;
|
||||
padding: var(--size);
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
inject,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
} 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, AfterViewInit
|
||||
{
|
||||
@ViewChild('passwordInput') passwordInput!: ElementRef<HTMLInputElement>;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.passwordInput.nativeElement.focus();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -41,10 +41,13 @@ export interface UnlockResponseMessage {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const debug = function (message: any) {
|
||||
const dateString = new Date().toISOString();
|
||||
console.log(`[Plebeian Signer - ${dateString}]: ${JSON.stringify(message)}`);
|
||||
};
|
||||
// Debug logging disabled - uncomment for development
|
||||
// export const debug = function (message: any) {
|
||||
// const dateString = new Date().toISOString();
|
||||
// console.log(`[Plebeian Signer - ${dateString}]: ${JSON.stringify(message)}`);
|
||||
// };
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
|
||||
export const debug = function (_message: any) {};
|
||||
|
||||
export type PromptResponse =
|
||||
| 'reject'
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
backgroundLogNip07Action,
|
||||
backgroundLogPermissionStored,
|
||||
NostrHelper,
|
||||
NwcClient,
|
||||
NwcConnection_DECRYPTED,
|
||||
@@ -441,7 +439,6 @@ async function processNip07Request(req: BackgroundRequestMessage): Promise<any>
|
||||
policy,
|
||||
req.params?.kind
|
||||
);
|
||||
await backgroundLogPermissionStored(req.host, req.method, policy, req.params?.kind);
|
||||
} else if (response === 'approve-all') {
|
||||
// P2: Store permission for ALL kinds/uses of this method from this host
|
||||
await storePermission(
|
||||
@@ -452,8 +449,6 @@ async function processNip07Request(req: BackgroundRequestMessage): Promise<any>
|
||||
'allow',
|
||||
undefined // undefined kind = allow all kinds for signEvent
|
||||
);
|
||||
await backgroundLogPermissionStored(req.host, req.method, 'allow', undefined);
|
||||
debug(`Stored approve-all permission for ${req.method} from ${req.host}`);
|
||||
} else if (response === 'reject-all') {
|
||||
// P2: Store deny permission for ALL uses of this method from this host
|
||||
await storePermission(
|
||||
@@ -464,15 +459,9 @@ async function processNip07Request(req: BackgroundRequestMessage): Promise<any>
|
||||
'deny',
|
||||
undefined
|
||||
);
|
||||
await backgroundLogPermissionStored(req.host, req.method, 'deny', undefined);
|
||||
debug(`Stored reject-all permission for ${req.method} from ${req.host}`);
|
||||
}
|
||||
|
||||
if (['reject', 'reject-once', 'reject-all'].includes(response)) {
|
||||
await backgroundLogNip07Action(req.method, req.host, false, false, {
|
||||
kind: req.params?.kind,
|
||||
peerPubkey: req.params?.peerPubkey,
|
||||
});
|
||||
throw new Error('Permission denied');
|
||||
}
|
||||
} else {
|
||||
@@ -481,71 +470,47 @@ async function processNip07Request(req: BackgroundRequestMessage): Promise<any>
|
||||
}
|
||||
|
||||
const relays: Relays = {};
|
||||
let result: any;
|
||||
|
||||
switch (req.method) {
|
||||
case 'getPublicKey':
|
||||
result = NostrHelper.pubkeyFromPrivkey(currentIdentity.privkey);
|
||||
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove);
|
||||
return result;
|
||||
return NostrHelper.pubkeyFromPrivkey(currentIdentity.privkey);
|
||||
|
||||
case 'signEvent':
|
||||
result = signEvent(req.params, currentIdentity.privkey);
|
||||
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
|
||||
kind: req.params?.kind,
|
||||
});
|
||||
return result;
|
||||
return signEvent(req.params, currentIdentity.privkey);
|
||||
|
||||
case 'getRelays':
|
||||
browserSessionData.relays.forEach((x) => {
|
||||
relays[x.url] = { read: x.read, write: x.write };
|
||||
});
|
||||
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove);
|
||||
return relays;
|
||||
|
||||
case 'nip04.encrypt':
|
||||
result = await nip04Encrypt(
|
||||
return await nip04Encrypt(
|
||||
currentIdentity.privkey,
|
||||
req.params.peerPubkey,
|
||||
req.params.plaintext
|
||||
);
|
||||
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
|
||||
peerPubkey: req.params.peerPubkey,
|
||||
});
|
||||
return result;
|
||||
|
||||
case 'nip44.encrypt':
|
||||
result = await nip44Encrypt(
|
||||
return await nip44Encrypt(
|
||||
currentIdentity.privkey,
|
||||
req.params.peerPubkey,
|
||||
req.params.plaintext
|
||||
);
|
||||
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
|
||||
peerPubkey: req.params.peerPubkey,
|
||||
});
|
||||
return result;
|
||||
|
||||
case 'nip04.decrypt':
|
||||
result = await nip04Decrypt(
|
||||
return await nip04Decrypt(
|
||||
currentIdentity.privkey,
|
||||
req.params.peerPubkey,
|
||||
req.params.ciphertext
|
||||
);
|
||||
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
|
||||
peerPubkey: req.params.peerPubkey,
|
||||
});
|
||||
return result;
|
||||
|
||||
case 'nip44.decrypt':
|
||||
result = await nip44Decrypt(
|
||||
return await nip44Decrypt(
|
||||
currentIdentity.privkey,
|
||||
req.params.peerPubkey,
|
||||
req.params.ciphertext
|
||||
);
|
||||
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
|
||||
peerPubkey: req.params.peerPubkey,
|
||||
});
|
||||
return result;
|
||||
|
||||
default:
|
||||
throw new Error(`Not supported request method '${req.method}'.`);
|
||||
@@ -625,7 +590,6 @@ async function processWeblnRequest(req: BackgroundRequestMessage): Promise<any>
|
||||
method,
|
||||
policy
|
||||
);
|
||||
await backgroundLogPermissionStored(req.host, method, policy);
|
||||
} else if (response === 'approve-all' && method !== 'webln.sendPayment' && method !== 'webln.keysend') {
|
||||
// P2: Store permission for all uses of this WebLN method
|
||||
await storePermission(
|
||||
@@ -635,8 +599,6 @@ async function processWeblnRequest(req: BackgroundRequestMessage): Promise<any>
|
||||
method,
|
||||
'allow'
|
||||
);
|
||||
await backgroundLogPermissionStored(req.host, method, 'allow');
|
||||
debug(`Stored approve-all permission for ${method} from ${req.host}`);
|
||||
}
|
||||
|
||||
if (['reject', 'reject-once', 'reject-all'].includes(response)) {
|
||||
|
||||
BIN
releases/plebeian-signer-chrome-v1.1.4.tar.gz
Normal file
BIN
releases/plebeian-signer-firefox-v1.1.4.tar.gz
Normal file
BIN
screenshots/chrome-cashu.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
screenshots/chrome-profile.png
Normal file
|
After Width: | Height: | Size: 242 KiB |
BIN
screenshots/chrome-sign.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
screenshots/firefox-cashu.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
screenshots/firefox-profile.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
screenshots/firefox-sign.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
screenshots/store-firefox/01-identity-management.png
Normal file
|
After Width: | Height: | Size: 771 KiB |
BIN
screenshots/store-firefox/02-cashu-wallet.png
Normal file
|
After Width: | Height: | Size: 254 KiB |
BIN
screenshots/store-firefox/03-signing-permissions.png
Normal file
|
After Width: | Height: | Size: 335 KiB |
77
screenshots/store-firefox/AMO_DESCRIPTION.txt
Normal file
@@ -0,0 +1,77 @@
|
||||
MOZILLA ADD-ONS (AMO) LISTING
|
||||
=============================
|
||||
|
||||
NAME: Plebeian Signer
|
||||
|
||||
SUMMARY (250 chars max):
|
||||
Manage multiple Nostr identities and sign events securely. Built-in Cashu ecash wallet for sending and receiving sats. Your private keys never leave the extension.
|
||||
|
||||
DESCRIPTION:
|
||||
------------
|
||||
|
||||
Plebeian Signer is a secure browser extension for managing your Nostr identities without exposing your private keys to web applications.
|
||||
|
||||
<b>Key Features</b>
|
||||
|
||||
<b>Multiple Identity Management</b>
|
||||
• Create and manage multiple Nostr identities in one place
|
||||
• Switch between profiles instantly when using Nostr apps
|
||||
• Import existing keys or generate new ones
|
||||
• Customize profiles with display names and metadata
|
||||
|
||||
<b>Secure Key Storage</b>
|
||||
• Private keys encrypted with Argon2id + AES-256-GCM
|
||||
• Keys never leave the extension - apps only receive signatures
|
||||
• Password-protected vault with automatic locking
|
||||
• Optional sync across browser instances
|
||||
|
||||
<b>NIP-07 Signing</b>
|
||||
• Full NIP-07 implementation (window.nostr interface)
|
||||
• Review event details before signing
|
||||
• Granular permission controls per site and event kind
|
||||
• One-click approve or reject with "always" options
|
||||
• Supports NIP-04 and NIP-44 encryption/decryption
|
||||
|
||||
<b>Built-in Cashu Wallet</b>
|
||||
• Store and manage ecash (Cashu tokens)
|
||||
• Send and receive tokens instantly
|
||||
• Deposit sats via Lightning invoices
|
||||
• Connect to multiple Cashu mints
|
||||
• View token history and check for spent proofs
|
||||
|
||||
<b>Privacy Focused</b>
|
||||
• No data collection or analytics
|
||||
• No external connections except to mints you configure
|
||||
• Fully open source
|
||||
• Works offline for signing operations
|
||||
|
||||
<b>Supported Nostr Apps</b>
|
||||
Works with any NIP-07 compatible application including Snort, Iris, Coracle, Nostrudel, Habla, and many more.
|
||||
|
||||
---
|
||||
|
||||
CATEGORIES:
|
||||
- Primary: Social & Communication
|
||||
- Secondary: Privacy & Security
|
||||
|
||||
TAGS: nostr, signing, identity, wallet, cashu, ecash, lightning, bitcoin, privacy, encryption
|
||||
|
||||
LICENSE: MIT
|
||||
|
||||
HOMEPAGE: https://git.mleku.dev/mleku/plebeian-signer
|
||||
|
||||
SUPPORT EMAIL: (your email)
|
||||
|
||||
PRIVACY POLICY: This extension does not collect, store, or transmit any user data to external servers. All data is stored locally in your browser using encrypted storage. The only external connections made are to Cashu mints that you explicitly configure.
|
||||
|
||||
---
|
||||
|
||||
SCREENSHOTS (1280x800):
|
||||
1. 01-identity-management.png - Multiple identity management
|
||||
2. 02-cashu-wallet.png - Built-in Cashu ecash wallet
|
||||
3. 03-signing-permissions.png - Secure event signing permissions
|
||||
|
||||
---
|
||||
|
||||
EXTENSION ID: plebian-signer@mleku.dev
|
||||
VERSION: 1.1.5
|
||||
BIN
screenshots/store/01-identity-management.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
screenshots/store/02-cashu-wallet.png
Normal file
|
After Width: | Height: | Size: 298 KiB |
BIN
screenshots/store/03-signing-permissions.png
Normal file
|
After Width: | Height: | Size: 320 KiB |
73
screenshots/store/STORE_DESCRIPTION.txt
Normal file
@@ -0,0 +1,73 @@
|
||||
CHROME WEB STORE LISTING
|
||||
========================
|
||||
|
||||
NAME: Plebeian Signer - Nostr Identity Manager & Signer
|
||||
|
||||
SHORT DESCRIPTION (132 chars max):
|
||||
Manage multiple Nostr identities, sign events securely, and store ecash with the built-in Cashu wallet. Your keys, your control.
|
||||
|
||||
DETAILED DESCRIPTION:
|
||||
---------------------
|
||||
|
||||
Plebeian Signer is a secure browser extension for managing your Nostr identities without exposing your private keys to web applications.
|
||||
|
||||
KEY FEATURES
|
||||
|
||||
Multiple Identity Management
|
||||
• Create and manage multiple Nostr identities in one place
|
||||
• Switch between profiles instantly when using Nostr apps
|
||||
• Import existing keys or generate new ones
|
||||
• Customize profiles with display names and metadata
|
||||
|
||||
Secure Key Storage
|
||||
• Private keys are encrypted with Argon2id + AES-256-GCM
|
||||
• Keys never leave the extension - apps only receive signatures
|
||||
• Password-protected vault with automatic locking
|
||||
• Optional sync across browser instances
|
||||
|
||||
NIP-07 Signing
|
||||
• Full NIP-07 implementation (window.nostr interface)
|
||||
• Review event details before signing
|
||||
• Granular permission controls per site and event kind
|
||||
• One-click approve or reject with "always" options
|
||||
• Supports NIP-04 and NIP-44 encryption/decryption
|
||||
|
||||
Built-in Cashu Wallet
|
||||
• Store and manage ecash (Cashu tokens)
|
||||
• Send and receive tokens instantly
|
||||
• Deposit sats via Lightning invoices
|
||||
• Connect to multiple Cashu mints
|
||||
• View token history and check for spent proofs
|
||||
|
||||
Privacy Focused
|
||||
• No data collection or analytics
|
||||
• No external connections except to mints you configure
|
||||
• Fully open source
|
||||
• Works offline for signing operations
|
||||
|
||||
PERFECT FOR
|
||||
• Nostr users managing multiple personas
|
||||
• Privacy-conscious users who want key isolation
|
||||
• Anyone tired of copy-pasting private keys
|
||||
• Users who want ecash functionality integrated with their identity
|
||||
|
||||
SUPPORTED NOSTR APPS
|
||||
Works with any NIP-07 compatible application including Snort, Iris, Coracle, Nostrudel, Habla, and many more.
|
||||
|
||||
OPEN SOURCE
|
||||
Plebeian Signer is free and open source software. Review the code, report issues, or contribute at the project repository.
|
||||
|
||||
---
|
||||
|
||||
CATEGORY: Social & Communication
|
||||
|
||||
LANGUAGE: English
|
||||
|
||||
PRIVACY POLICY: Not required (no user data collected)
|
||||
|
||||
---
|
||||
|
||||
SCREENSHOTS (1280x800):
|
||||
1. 01-identity-management.png - Multiple identity management
|
||||
2. 02-cashu-wallet.png - Built-in Cashu ecash wallet
|
||||
3. 03-signing-permissions.png - Secure event signing permissions
|
||||