Make pin optional
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Kieran 2023-10-05 14:02:41 +01:00
parent c162ac6428
commit 4342669896
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
12 changed files with 236 additions and 116 deletions

View File

@ -3,7 +3,7 @@ import "./PinPrompt.css";
import { ReactNode, useRef, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { unwrap } from "@snort/shared";
import { EventPublisher, InvalidPinError, PinEncrypted, PinEncryptedPayload } from "@snort/system";
import { EventPublisher, InvalidPinError, PinEncrypted } from "@snort/system";
import useEventPublisher from "Hooks/useEventPublisher";
import { LoginStore, createPublisher, sessionNeedsPin } from "Login";
@ -67,7 +67,7 @@ export function PinPrompt({
<h2>
<FormattedMessage defaultMessage="Enter Pin" />
</h2>
{subTitle}
{subTitle ? <div>{subTitle}</div> : null}
<input
type="number"
onChange={e => setPin(e.target.value)}
@ -113,9 +113,9 @@ export function LoginUnlock() {
}
async function unlockSession(pin: string) {
const key = new PinEncrypted(unwrap(login.privateKeyData) as PinEncryptedPayload);
await key.decrypt(pin);
const pub = createPublisher(login, key);
const key = unwrap(login.privateKeyData);
await key.unlock(pin);
const pub = createPublisher(login);
if (pub) {
if (login.preferences.pow) {
pub.pow(login.preferences.pow, new WasmPowWorker());

View File

@ -1,5 +1,5 @@
import { useIntl } from "react-intl";
import { Nip46Signer, PinEncrypted } from "@snort/system";
import { Nip46Signer, KeyStorage } from "@snort/system";
import { EmailRegex, MnemonicRegex } from "Const";
import { LoginSessionType, LoginStore } from "Login";
@ -8,13 +8,11 @@ import { getNip05PubKey } from "Pages/LoginPage";
import { bech32ToHex } from "SnortUtils";
import { unwrap } from "@snort/shared";
export class PinRequiredError extends Error {}
export default function useLoginHandler() {
const { formatMessage } = useIntl();
const hasSubtleCrypto = window.crypto.subtle !== undefined;
async function doLogin(key: string, pin?: string) {
async function doLogin(key: string, pin: (key: string) => Promise<KeyStorage>) {
const insecureMsg = formatMessage({
defaultMessage:
"Can't login with private key on an insecure connection, please use a Nostr key manager extension instead",
@ -26,8 +24,7 @@ export default function useLoginHandler() {
}
const hexKey = bech32ToHex(key);
if (hexKey.length === 64) {
if (!pin) throw new PinRequiredError();
LoginStore.loginWithPrivateKey(await PinEncrypted.create(hexKey, pin));
LoginStore.loginWithPrivateKey(await pin(hexKey));
return;
} else {
throw new Error("INVALID PRIVATE KEY");
@ -36,17 +33,15 @@ export default function useLoginHandler() {
if (!hasSubtleCrypto) {
throw new Error(insecureMsg);
}
if (!pin) throw new PinRequiredError();
const ent = generateBip39Entropy(key);
const keyHex = entropyToPrivateKey(ent);
LoginStore.loginWithPrivateKey(await PinEncrypted.create(keyHex, pin));
const hexKey = entropyToPrivateKey(ent);
LoginStore.loginWithPrivateKey(await pin(hexKey));
return;
} else if (key.length === 64) {
if (!hasSubtleCrypto) {
throw new Error(insecureMsg);
}
if (!pin) throw new PinRequiredError();
LoginStore.loginWithPrivateKey(await PinEncrypted.create(key, pin));
LoginStore.loginWithPrivateKey(await pin(key));
return;
}
@ -58,7 +53,6 @@ export default function useLoginHandler() {
const hexKey = await getNip05PubKey(key);
LoginStore.loginWithPubkey(hexKey, LoginSessionType.PublicKey);
} else if (key.startsWith("bunker://")) {
if (!pin) throw new PinRequiredError();
const nip46 = new Nip46Signer(key);
await nip46.init();
@ -68,7 +62,7 @@ export default function useLoginHandler() {
LoginSessionType.Nip46,
undefined,
nip46.relays,
await PinEncrypted.create(unwrap(nip46.privateKey), pin),
await pin(unwrap(nip46.privateKey)),
);
nip46.close();
} else {

View File

@ -1,4 +1,4 @@
import { RelaySettings, EventPublisher, PinEncrypted, Nip46Signer, Nip7Signer, PrivateKeySigner } from "@snort/system";
import { RelaySettings, EventPublisher, Nip46Signer, Nip7Signer, PrivateKeySigner, KeyStorage } from "@snort/system";
import { unixNowMs } from "@snort/shared";
import * as secp from "@noble/curves/secp256k1";
import * as utils from "@noble/curves/abstract/utils";
@ -10,7 +10,6 @@ import { bech32ToHex, dedupeById, randomSample, sanitizeRelayUrl, unwrap } from
import { SubscriptionEvent } from "Subscription";
import { System } from "index";
import { Chats, FollowsFeed, GiftsCache, Notifications } from "Cache";
import { PinRequiredError } from "Hooks/useLoginHandler";
import { Nip7OsSigner } from "./Nip7OsSigner";
export function setRelays(state: LoginSession, relays: Record<string, RelaySettings>, createdAt: number) {
@ -64,7 +63,7 @@ export function clearEntropy(state: LoginSession) {
/**
* Generate a new key and login with this generated key
*/
export async function generateNewLogin(pin: string) {
export async function generateNewLogin(pin: (key: string) => Promise<KeyStorage>) {
const ent = generateBip39Entropy();
const entropy = utils.bytesToHex(ent);
const privateKey = entropyToPrivateKey(ent);
@ -89,9 +88,7 @@ export async function generateNewLogin(pin: string) {
const publisher = EventPublisher.privateKey(privateKey);
const ev = await publisher.contactList([bech32ToHex(SnortPubKey), publicKey], newRelays);
System.BroadcastEvent(ev);
const key = await PinEncrypted.create(privateKey, pin);
LoginStore.loginWithPrivateKey(key, entropy, newRelays);
LoginStore.loginWithPrivateKey(await pin(privateKey), entropy, newRelays);
}
export function generateRandomKey() {
@ -175,22 +172,17 @@ export function addSubscription(state: LoginSession, ...subs: SubscriptionEvent[
}
export function sessionNeedsPin(l: LoginSession) {
return l.type === LoginSessionType.PrivateKey || l.type === LoginSessionType.Nip46;
return l.privateKeyData && l.privateKeyData.shouldUnlock();
}
export function createPublisher(l: LoginSession, pin?: PinEncrypted) {
export function createPublisher(l: LoginSession) {
switch (l.type) {
case LoginSessionType.PrivateKey: {
if (!pin) throw new PinRequiredError();
l.privateKeyData = pin;
return EventPublisher.privateKey(pin.value);
return EventPublisher.privateKey(unwrap(l.privateKeyData as KeyStorage).value);
}
case LoginSessionType.Nip46: {
if (!pin) throw new PinRequiredError();
l.privateKeyData = pin;
const relayArgs = (l.remoteSignerRelays ?? []).map(a => `relay=${encodeURIComponent(a)}`);
const inner = new PrivateKeySigner(pin.value);
const inner = new PrivateKeySigner(unwrap(l.privateKeyData as KeyStorage).value);
const nip46 = new Nip46Signer(`bunker://${unwrap(l.publicKey)}?${[...relayArgs].join("&")}`, inner);
return new EventPublisher(nip46, unwrap(l.publicKey));
}

View File

@ -1,4 +1,4 @@
import { HexKey, RelaySettings, u256, PinEncrypted, PinEncryptedPayload } from "@snort/system";
import { HexKey, RelaySettings, u256, KeyStorage } from "@snort/system";
import { UserPreferences } from "Login";
import { SubscriptionEvent } from "Subscription";
@ -47,7 +47,7 @@ export interface LoginSession {
/**
* Encrypted private key
*/
privateKeyData?: PinEncrypted | PinEncryptedPayload;
privateKeyData?: KeyStorage;
/**
* BIP39-generated, hex-encoded entropy

View File

@ -2,7 +2,7 @@ import * as secp from "@noble/curves/secp256k1";
import * as utils from "@noble/curves/abstract/utils";
import { v4 as uuid } from "uuid";
import { HexKey, RelaySettings, PinEncrypted, EventPublisher } from "@snort/system";
import { HexKey, RelaySettings, EventPublisher, KeyStorage, NotEncrypted } from "@snort/system";
import { deepClone, sanitizeRelayUrl, unwrap, ExternalStore } from "@snort/shared";
import { DefaultRelays } from "Const";
@ -85,6 +85,9 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
timestamp: 0,
};
v.extraChats ??= [];
if (v.privateKeyData) {
v.privateKeyData = KeyStorage.fromPayload(v.privateKeyData as object);
}
}
this.#loadIrisKeyIfExists();
}
@ -121,7 +124,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
type: LoginSessionType,
relays?: Record<string, RelaySettings>,
remoteSignerRelays?: Array<string>,
privateKey?: PinEncrypted,
privateKey?: KeyStorage,
) {
if (this.#accounts.has(key)) {
throw new Error("Already logged in with this pubkey");
@ -159,7 +162,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
return Object.fromEntries(DefaultRelays.entries());
}
loginWithPrivateKey(key: PinEncrypted, entropy?: string, relays?: Record<string, RelaySettings>) {
loginWithPrivateKey(key: KeyStorage, entropy?: string, relays?: Record<string, RelaySettings>) {
const pubKey = utils.bytesToHex(secp.schnorr.getPublicKey(key.value));
if (this.#accounts.has(pubKey)) {
throw new Error("Already logged in with this pubkey");
@ -218,13 +221,13 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
return { ...s };
}
async #loadIrisKeyIfExists() {
#loadIrisKeyIfExists() {
try {
const irisKeyJSON = window.localStorage.getItem("iris.myKey");
if (irisKeyJSON) {
const irisKeyObj = JSON.parse(irisKeyJSON);
if (irisKeyObj.priv) {
const privateKey = await PinEncrypted.create(irisKeyObj.priv, "1234");
const privateKey = new NotEncrypted(irisKeyObj.priv);
this.loginWithPrivateKey(privateKey);
window.localStorage.removeItem("iris.myKey");
}
@ -286,7 +289,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
}
const toSave = [];
for (const v of this.#accounts.values()) {
if (v.privateKeyData instanceof PinEncrypted) {
if (v.privateKeyData instanceof KeyStorage) {
toSave.push({
...v,
privateKeyData: v.privateKeyData.toPayload(),

View File

@ -3,15 +3,15 @@ import "./LoginPage.css";
import { CSSProperties, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useIntl, FormattedMessage } from "react-intl";
import { HexKey, Nip46Signer, PinEncrypted, PrivateKeySigner } from "@snort/system";
import { HexKey, Nip46Signer, NotEncrypted, PinEncrypted, PrivateKeySigner } from "@snort/system";
import { bech32ToHex, getPublicKey, unwrap } from "SnortUtils";
import { bech32ToHex, getPublicKey, isHex, unwrap } from "SnortUtils";
import ZapButton from "Element/Event/ZapButton";
import useImgProxy from "Hooks/useImgProxy";
import Icon from "Icons/Icon";
import { generateNewLogin, LoginSessionType, LoginStore } from "Login";
import AsyncButton from "Element/AsyncButton";
import useLoginHandler, { PinRequiredError } from "Hooks/useLoginHandler";
import useLoginHandler from "Hooks/useLoginHandler";
import { secp256k1 } from "@noble/curves/secp256k1";
import { bytesToHex } from "@noble/curves/abstract/utils";
import Modal from "Element/Modal";
@ -94,16 +94,20 @@ export default function LoginPage() {
setArt({ ...ret, link: url });
}, []);
async function makeKeyStore(key: string, pin?: string) {
if (pin) {
return await PinEncrypted.create(key, pin);
} else {
return new NotEncrypted(key);
}
}
async function doLogin(pin?: string) {
setError("");
try {
await loginHandler.doLogin(key, pin);
await loginHandler.doLogin(key, key => makeKeyStore(key, pin));
navigate("/");
} catch (e) {
if (e instanceof PinRequiredError) {
setPin(true);
return;
}
if (e instanceof Error) {
setError(e.message);
} else {
@ -117,9 +121,9 @@ export default function LoginPage() {
}
}
async function makeRandomKey(pin: string) {
async function makeRandomKey(pin?: string) {
try {
await generateNewLogin(pin);
await generateNewLogin(key => makeKeyStore(key, pin));
window.plausible?.("Generate Account");
navigate("/new");
} catch (e) {
@ -153,7 +157,7 @@ export default function LoginPage() {
setNip46Key(newKey);
}
async function startNip46(pin: string) {
async function startNip46(pin?: string) {
if (!nostrConnect || !nip46Key) return;
const signer = new Nip46Signer(nostrConnect, new PrivateKeySigner(nip46Key));
@ -165,7 +169,7 @@ export default function LoginPage() {
LoginSessionType.Nip46,
undefined,
["wss://relay.damus.io"],
await PinEncrypted.create(nip46Key, pin),
await makeKeyStore(nip46Key, pin),
);
navigate("/");
}
@ -316,7 +320,15 @@ export default function LoginPage() {
/>
</p>
<div dir="auto" className="login-actions">
<AsyncButton type="button" onClick={() => doLogin()}>
<AsyncButton
type="button"
onClick={async () => {
if (key.startsWith("nsec") || (key.length === 64 && isHex(key))) {
setPin(true);
} else {
await doLogin();
}
}}>
<FormattedMessage defaultMessage="Login" description="Login button" />
</AsyncButton>
<AsyncButton onClick={() => setPin(true)}>
@ -325,9 +337,22 @@ export default function LoginPage() {
{pin && (
<PinPrompt
subTitle={
<p>
<FormattedMessage defaultMessage="Enter a pin to encrypt your private key, you must enter this pin every time you open Snort." />
</p>
<>
<p>
<FormattedMessage
defaultMessage="Secure your private key with a PIN, ensuring enhanced protection on {site}. You'll be prompted to enter this PIN each time you access the site."
values={{
site: process.env.APP_NAME_CAPITALIZED,
}}
/>
</p>
<p>
<FormattedMessage defaultMessage="Alternatively, you may choose to store your private key without a PIN by selecting 'Cancel.'" />
</p>
<p>
<FormattedMessage defaultMessage="After submitting the pin there may be a slight delay as we encrypt the key." />
</p>
</>
}
onResult={async pin => {
setPin(false);
@ -339,7 +364,16 @@ export default function LoginPage() {
await makeRandomKey(pin);
}
}}
onCancel={() => setPin(false)}
onCancel={async () => {
setPin(false);
if (key) {
await doLogin();
} else if (nostrConnect) {
await startNip46();
} else {
await makeRandomKey();
}
}}
/>
)}
{altLogins()}

View File

@ -3,15 +3,13 @@ import { useNavigate } from "react-router-dom";
import Logo from "Element/Logo";
import { CollapsedSection } from "Element/Collapsed";
import Copy from "Element/Copy";
import { hexToBech32 } from "SnortUtils";
import { hexToMnemonic } from "nip6";
import useLogin from "Hooks/useLogin";
import { PROFILE } from ".";
import { DefaultPreferences, LoginStore, updatePreferences } from "Login";
import { AllLanguageCodes } from "Pages/settings/Preferences";
import messages from "./messages";
import ExportKeys from "Pages/settings/Keys";
const WhatIsSnort = () => {
return (
@ -127,14 +125,7 @@ export default function NewUserFlow() {
<p>
<FormattedMessage {...messages.SaveKeysHelp} />
</p>
<h2>
<FormattedMessage {...messages.YourPubkey} />
</h2>
<Copy text={hexToBech32("npub", login.publicKey ?? "")} />
<h2>
<FormattedMessage {...messages.YourMnemonic} />
</h2>
<Copy text={hexToMnemonic(login.generatedEntropy ?? "")} />
<ExportKeys />
<div className="next-actions">
<button
type="button"

View File

@ -3,3 +3,28 @@
border: 2px dashed #222222;
border-radius: 16px;
}
.mnemonic-grid {
display: grid;
text-align: center;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.mnemonic-grid > div {
border: 1px solid #222222;
border-radius: 5px;
overflow: hidden;
user-select: none;
}
.mnemonic-grid .word > div:nth-of-type(1) {
background-color: var(--gray);
padding: 4px 8px;
min-width: 1.5em;
font-variant-numeric: ordinal;
}
.mnemonic-grid .word > div:nth-of-type(2) {
flex-grow: 1;
}

View File

@ -1,6 +1,6 @@
import "./Keys.css";
import FormattedMessage from "Element/FormattedMessage";
import { encodeTLV, NostrPrefix, PinEncrypted } from "@snort/system";
import { encodeTLV, KeyStorage, NostrPrefix } from "@snort/system";
import Copy from "Element/Copy";
import useLogin from "Hooks/useLogin";
@ -11,25 +11,34 @@ export default function ExportKeys() {
const { publicKey, privateKeyData, generatedEntropy } = useLogin();
return (
<div className="flex-column g12">
<h3>
<h2>
<FormattedMessage defaultMessage="Public Key" />
</h3>
</h2>
<Copy text={hexToBech32("npub", publicKey ?? "")} className="dashed" />
<Copy text={encodeTLV(NostrPrefix.Profile, publicKey ?? "")} className="dashed" />
{privateKeyData instanceof PinEncrypted && (
{privateKeyData instanceof KeyStorage && (
<>
<h3>
<h2>
<FormattedMessage defaultMessage="Private Key" />
</h3>
</h2>
<Copy text={hexToBech32("nsec", privateKeyData.value)} className="dashed" />
</>
)}
{generatedEntropy && (
<>
<h3>
<h2>
<FormattedMessage defaultMessage="Mnemonic" />
</h3>
<Copy text={hexToMnemonic(generatedEntropy ?? "")} className="dashed" />
</h2>
<div className="mnemonic-grid">
{hexToMnemonic(generatedEntropy ?? "")
.split(" ")
.map((a, i) => (
<div className="flex word">
<div>{i + 1}</div>
<div>{a}</div>
</div>
))}
</div>
</>
)}
</div>

View File

@ -72,9 +72,6 @@
"0yO7wF": {
"defaultMessage": "{n} secs"
},
"1A7TZk": {
"defaultMessage": "What is Snort and how does it work?"
},
"1Mo59U": {
"defaultMessage": "Are you sure you want to remove this note from bookmarks?"
},
@ -208,6 +205,9 @@
"6OSOXl": {
"defaultMessage": "Reason: <i>{reason}</i>"
},
"6TfgXX": {
"defaultMessage": "{site} is an open source project built by passionate people in their free time"
},
"6Yfvvp": {
"defaultMessage": "Get an identifier"
},
@ -223,6 +223,9 @@
"7+Domh": {
"defaultMessage": "Notes"
},
"7/h1jn": {
"defaultMessage": "After submitting the pin there may be a slight delay as we encrypt the key."
},
"7BX/yC": {
"defaultMessage": "Account Switcher"
},
@ -323,9 +326,6 @@
"BOUMjw": {
"defaultMessage": "No nostr users found for {twitterUsername}"
},
"BOr9z/": {
"defaultMessage": "Snort is an open source project built by passionate people in their free time"
},
"BWpuKl": {
"defaultMessage": "Update"
},
@ -356,6 +356,9 @@
"CmZ9ls": {
"defaultMessage": "{n} Muted"
},
"CoVXRS": {
"defaultMessage": "Alternatively, you may choose to store your private key without a PIN by selecting 'Cancel.'"
},
"CsCUYo": {
"defaultMessage": "{n} sats"
},
@ -507,9 +510,6 @@
"HhcAVH": {
"defaultMessage": "You don't follow this person, click here to load media from <i>{link}</i>, or update <a><i>your preferences</i></a> to always load media from everybody."
},
"IDjHJ6": {
"defaultMessage": "Thanks for using Snort, please consider donating if you can."
},
"IEwZvs": {
"defaultMessage": "Are you sure you want to unpin this note?"
},
@ -686,6 +686,12 @@
"ORGv1Q": {
"defaultMessage": "Created"
},
"Oq/kVn": {
"defaultMessage": "Name-squatting and impersonation is not allowed. {site} and our partners reserve the right to terminate your handle (not your account - nobody can take that away) for violating this rule."
},
"P/xrLk": {
"defaultMessage": "Secure your private key with a PIN, ensuring enhanced protection on {site}. You'll be prompted to enter this PIN each time you access the site."
},
"P61BTu": {
"defaultMessage": "Copy Event JSON"
},
@ -701,6 +707,9 @@
"PLSbmL": {
"defaultMessage": "Your mnemonic phrase"
},
"PaN7t3": {
"defaultMessage": "Preview on {site}"
},
"PamNxw": {
"defaultMessage": "Unknown file header: {name}"
},
@ -761,6 +770,9 @@
"defaultMessage": "Sort",
"description": "Label for sorting options for people search"
},
"SLZGPn": {
"defaultMessage": "Enter a pin to encrypt your private key, you must enter this pin every time you open {site}."
},
"SMO+on": {
"defaultMessage": "Send zap to {name}"
},
@ -880,9 +892,6 @@
"XzF0aC": {
"defaultMessage": "Key manager extensions are more secure and allow you to easily login to any Nostr client, here are some well known extensions:"
},
"Y31HTH": {
"defaultMessage": "Help fund the development of Snort"
},
"YDURw6": {
"defaultMessage": "Service URL"
},
@ -950,9 +959,6 @@
"defaultMessage": "Install Extension",
"description": "Heading for install key manager extension"
},
"c2DTVd": {
"defaultMessage": "Enter a pin to encrypt your private key, you must enter this pin every time you open Snort."
},
"c35bj2": {
"defaultMessage": "If you have an enquiry about your NIP-05 order please DM {link}"
},
@ -1021,6 +1027,9 @@
"fBI91o": {
"defaultMessage": "Zap"
},
"fBlba3": {
"defaultMessage": "Thanks for using {site}, please consider donating if you can."
},
"fOksnD": {
"defaultMessage": "Can't vote because LNURL service does not support zaps"
},
@ -1048,9 +1057,6 @@
"gBdUXk": {
"defaultMessage": "Save your keys!"
},
"gDZkld": {
"defaultMessage": "Snort is a Nostr UI, nostr is a decentralised protocol for saving and distributing \"notes\"."
},
"gDzDRs": {
"defaultMessage": "Emoji to send when reactiong to a note"
},
@ -1120,9 +1126,6 @@
"jA3OE/": {
"defaultMessage": "{n,plural,=1{{n} sat} other{{n} sats}}"
},
"jCA7Cw": {
"defaultMessage": "Preview on snort"
},
"jMzO1S": {
"defaultMessage": "Internal error: {msg}"
},
@ -1151,6 +1154,9 @@
"kJYo0u": {
"defaultMessage": "{n,plural,=0{{name} reposted} other{{name} & {n} others reposted}}"
},
"kTLGM2": {
"defaultMessage": "{site} is designed to have a similar experience to Twitter."
},
"kaaf1E": {
"defaultMessage": "now"
},
@ -1175,6 +1181,9 @@
"lTbT3s": {
"defaultMessage": "Wallet password"
},
"lVKH7C": {
"defaultMessage": "What is {site} and how does it work?"
},
"lgg1KN": {
"defaultMessage": "account page"
},
@ -1230,6 +1239,9 @@
"nWQFic": {
"defaultMessage": "Renew"
},
"ncbgUU": {
"defaultMessage": "{site} is a Nostr UI, nostr is a decentralised protocol for saving and distributing \"notes\"."
},
"nn1qb3": {
"defaultMessage": "Your donations are greatly appreciated"
},
@ -1340,15 +1352,9 @@
"sUNhQE": {
"defaultMessage": "user"
},
"sWnYKw": {
"defaultMessage": "Snort is designed to have a similar experience to Twitter."
},
"sZQzjQ": {
"defaultMessage": "Failed to parse zap split: {input}"
},
"svOoEH": {
"defaultMessage": "Name-squatting and impersonation is not allowed. Snort and our partners reserve the right to terminate your handle (not your account - nobody can take that away) for violating this rule."
},
"tOdNiY": {
"defaultMessage": "Dark"
},
@ -1473,6 +1479,9 @@
"defaultMessage": "Read global from",
"description": "Label for reading global feed from specific relays"
},
"yNBPJp": {
"defaultMessage": "Help fund the development of {site}"
},
"zCb8fX": {
"defaultMessage": "Weight"
},

View File

@ -23,7 +23,6 @@
"0mch2Y": "name has disallowed characters",
"0uoY11": "Show Status",
"0yO7wF": "{n} secs",
"1A7TZk": "What is Snort and how does it work?",
"1Mo59U": "Are you sure you want to remove this note from bookmarks?",
"1R43+L": "Enter Nostr Wallet Connect config",
"1c4YST": "Connected to: {node} 🎉",
@ -68,11 +67,13 @@
"6/hB3S": "Watch Replay",
"65BmHb": "Failed to proxy image from {host}, click here to load directly",
"6OSOXl": "Reason: <i>{reason}</i>",
"6TfgXX": "{site} is an open source project built by passionate people in their free time",
"6Yfvvp": "Get an identifier",
"6bgpn+": "Not all clients support this, you may still receive some zaps as if zap splits was not configured",
"6ewQqw": "Likes ({n})",
"6uMqL1": "Unpaid",
"7+Domh": "Notes",
"7/h1jn": "After submitting the pin there may be a slight delay as we encrypt the key.",
"7BX/yC": "Account Switcher",
"7hp70g": "NIP-05",
"8/vBbP": "Reposts ({n})",
@ -105,7 +106,6 @@
"B6H7eJ": "nsec, npub, nip-05, hex",
"BGCM48": "Write access to Snort relay, with 1 year of event retention",
"BOUMjw": "No nostr users found for {twitterUsername}",
"BOr9z/": "Snort is an open source project built by passionate people in their free time",
"BWpuKl": "Update",
"BcGMo+": "Notes hold text content, the most popular usage of these notes is to store \"tweet like\" messages.",
"BjNwZW": "Nostr address (nip05)",
@ -116,6 +116,7 @@
"CHTbO3": "Failed to load invoice",
"CVWeJ6": "Trending People",
"CmZ9ls": "{n} Muted",
"CoVXRS": "Alternatively, you may choose to store your private key without a PIN by selecting 'Cancel.'",
"CsCUYo": "{n} sats",
"Cu/K85": "Translated from {lang}",
"D+KzKd": "Automatically zap every note when loaded",
@ -166,7 +167,6 @@
"HWbkEK": "Clear cache and reload",
"HbefNb": "Open Wallet",
"HhcAVH": "You don't follow this person, click here to load media from <i>{link}</i>, or update <a><i>your preferences</i></a> to always load media from everybody.",
"IDjHJ6": "Thanks for using Snort, please consider donating if you can.",
"IEwZvs": "Are you sure you want to unpin this note?",
"IKKHqV": "Follows",
"INSqIz": "Twitter username...",
@ -225,11 +225,14 @@
"OQSOJF": "Get a free nostr address",
"OQXnew": "You subscription is still active, you can't renew yet",
"ORGv1Q": "Created",
"Oq/kVn": "Name-squatting and impersonation is not allowed. {site} and our partners reserve the right to terminate your handle (not your account - nobody can take that away) for violating this rule.",
"P/xrLk": "Secure your private key with a PIN, ensuring enhanced protection on {site}. You'll be prompted to enter this PIN each time you access the site.",
"P61BTu": "Copy Event JSON",
"P7FD0F": "System (Default)",
"P7nJT9": "Total today (UTC): {amount} sats",
"PCSt5T": "Preferences",
"PLSbmL": "Your mnemonic phrase",
"PaN7t3": "Preview on {site}",
"PamNxw": "Unknown file header: {name}",
"Pe0ogR": "Theme",
"PrsIg7": "Reactions will be shown on every page, if disabled no reactions will be shown",
@ -249,6 +252,7 @@
"RoOyAh": "Relays",
"Rs4kCE": "Bookmark",
"RwFaYs": "Sort",
"SLZGPn": "Enter a pin to encrypt your private key, you must enter this pin every time you open {site}.",
"SMO+on": "Send zap to {name}",
"SOqbe9": "Update Lightning Address",
"SP0+yi": "Buy Subscription",
@ -288,7 +292,6 @@
"Xopqkl": "Your default zap amount is {number} sats, example values are calculated from this.",
"XrSk2j": "Redeem",
"XzF0aC": "Key manager extensions are more secure and allow you to easily login to any Nostr client, here are some well known extensions:",
"Y31HTH": "Help fund the development of Snort",
"YDURw6": "Service URL",
"YXA3AH": "Enable reactions",
"Z0FDj+": "Subscribe to Snort {plan} for {price} and receive the following rewards",
@ -311,7 +314,6 @@
"bxv59V": "Just now",
"c+JYNI": "No thanks",
"c+oiJe": "Install Extension",
"c2DTVd": "Enter a pin to encrypt your private key, you must enter this pin every time you open Snort.",
"c35bj2": "If you have an enquiry about your NIP-05 order please DM {link}",
"c3g2hL": "Broadcast Again",
"cFbU1B": "Using Alby? Go to {link} to get your NWC config!",
@ -334,6 +336,7 @@
"eSzf2G": "A single zap of {nIn} sats will allocate {nOut} sats to the zap pool.",
"eXT2QQ": "Group Chat",
"fBI91o": "Zap",
"fBlba3": "Thanks for using {site}, please consider donating if you can.",
"fOksnD": "Can't vote because LNURL service does not support zaps",
"fWZYP5": "Pinned",
"filwqD": "Read",
@ -343,7 +346,6 @@
"g5pX+a": "About",
"g985Wp": "Failed to send vote",
"gBdUXk": "Save your keys!",
"gDZkld": "Snort is a Nostr UI, nostr is a decentralised protocol for saving and distributing \"notes\".",
"gDzDRs": "Emoji to send when reactiong to a note",
"gXgY3+": "Not all clients support this yet",
"gczcC5": "Subscribe",
@ -367,7 +369,6 @@
"itPgxd": "Profile",
"izWS4J": "Unfollow",
"jA3OE/": "{n,plural,=1{{n} sat} other{{n} sats}}",
"jCA7Cw": "Preview on snort",
"jMzO1S": "Internal error: {msg}",
"jfV8Wr": "Back",
"juhqvW": "Improve login security with browser extensions",
@ -377,6 +378,7 @@
"k7sKNy": "Our very own NIP-05 verification service, help support the development of this site and get a shiny special badge on our site!",
"kEZUR8": "Register an Iris username",
"kJYo0u": "{n,plural,=0{{name} reposted} other{{name} & {n} others reposted}}",
"kTLGM2": "{site} is designed to have a similar experience to Twitter.",
"kaaf1E": "now",
"kuPHYE": "{n,plural,=0{{name} liked} other{{name} & {n} others liked}}",
"l+ikU1": "Everything in {plan}",
@ -385,6 +387,7 @@
"lD3+8a": "Pay",
"lPWASz": "Snort nostr address",
"lTbT3s": "Wallet password",
"lVKH7C": "What is {site} and how does it work?",
"lgg1KN": "account page",
"ll3xBp": "Image proxy service",
"lnaT9F": "Following {n}",
@ -403,6 +406,7 @@
"nN9XTz": "Share your thoughts with {link}",
"nOaArs": "Setup Profile",
"nWQFic": "Renew",
"ncbgUU": "{site} is a Nostr UI, nostr is a decentralised protocol for saving and distributing \"notes\".",
"nn1qb3": "Your donations are greatly appreciated",
"nwZXeh": "{n} blocked",
"o6Uy3d": "Only the secret key can be used to publish (sign events), everything else logs you in read-only mode.",
@ -439,9 +443,7 @@
"rx1i0i": "Short link",
"sKDn4e": "Show Badges",
"sUNhQE": "user",
"sWnYKw": "Snort is designed to have a similar experience to Twitter.",
"sZQzjQ": "Failed to parse zap split: {input}",
"svOoEH": "Name-squatting and impersonation is not allowed. Snort and our partners reserve the right to terminate your handle (not your account - nobody can take that away) for violating this rule.",
"tOdNiY": "Dark",
"th5lxp": "Send note to a subset of your write relays",
"thnRpU": "Getting NIP-05 verified can help:",
@ -482,6 +484,7 @@
"y1Z3or": "Language",
"yCLnBC": "LNURL or Lightning Address",
"yCmnnm": "Read global from",
"yNBPJp": "Help fund the development of {site}",
"zCb8fX": "Weight",
"zFegDD": "Contact",
"zINlao": "Owner",

View File

@ -11,15 +11,44 @@ export class InvalidPinError extends Error {
}
}
export abstract class KeyStorage {
// Raw value
abstract get value(): string;
/**
* Is the storage locked
*/
abstract shouldUnlock(): boolean;
abstract unlock(code: string): Promise<void>;
/**
* Get a payload object which can be serialized to JSON
*/
abstract toPayload(): Object;
/**
* Create a key storage class from its payload
*/
static fromPayload(o: object) {
if ("raw" in o && typeof o.raw === "string") {
return new NotEncrypted(o.raw);
} else {
return new PinEncrypted(o as unknown as PinEncryptedPayload);
}
}
}
/**
* Pin protected data
*/
export class PinEncrypted {
export class PinEncrypted extends KeyStorage {
static readonly #opts = { N: 2 ** 20, r: 8, p: 1, dkLen: 32 };
#decrypted?: Uint8Array;
#encrypted: PinEncryptedPayload;
constructor(enc: PinEncryptedPayload) {
super();
this.#encrypted = enc;
}
@ -28,7 +57,11 @@ export class PinEncrypted {
return bytesToHex(this.#decrypted);
}
async decrypt(pin: string) {
override shouldUnlock(): boolean {
return !this.#decrypted;
}
override async unlock(pin: string) {
const key = await scryptAsync(pin, base64.decode(this.#encrypted.salt), PinEncrypted.#opts);
const ciphertext = base64.decode(this.#encrypted.ciphertext);
const nonce = base64.decode(this.#encrypted.iv);
@ -61,6 +94,33 @@ export class PinEncrypted {
}
}
export class NotEncrypted extends KeyStorage {
#key: string;
constructor(key: string) {
super();
this.#key = key;
}
get value() {
return this.#key;
}
override shouldUnlock(): boolean {
return false;
}
override unlock(code: string): Promise<void> {
throw new Error("Method not implemented.");
}
override toPayload(): Object {
return {
raw: this.#key,
};
}
}
export interface PinEncryptedPayload {
salt: string; // for KDF
ciphertext: string;