Make pin optional
This commit is contained in:
parent
c162ac6428
commit
4342669896
@ -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());
|
||||
|
@ -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 {
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
|
@ -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()}
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user