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

View File

@ -1,5 +1,5 @@
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { Nip46Signer, PinEncrypted } from "@snort/system"; import { Nip46Signer, KeyStorage } from "@snort/system";
import { EmailRegex, MnemonicRegex } from "Const"; import { EmailRegex, MnemonicRegex } from "Const";
import { LoginSessionType, LoginStore } from "Login"; import { LoginSessionType, LoginStore } from "Login";
@ -8,13 +8,11 @@ import { getNip05PubKey } from "Pages/LoginPage";
import { bech32ToHex } from "SnortUtils"; import { bech32ToHex } from "SnortUtils";
import { unwrap } from "@snort/shared"; import { unwrap } from "@snort/shared";
export class PinRequiredError extends Error {}
export default function useLoginHandler() { export default function useLoginHandler() {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const hasSubtleCrypto = window.crypto.subtle !== undefined; 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({ const insecureMsg = formatMessage({
defaultMessage: defaultMessage:
"Can't login with private key on an insecure connection, please use a Nostr key manager extension instead", "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); const hexKey = bech32ToHex(key);
if (hexKey.length === 64) { if (hexKey.length === 64) {
if (!pin) throw new PinRequiredError(); LoginStore.loginWithPrivateKey(await pin(hexKey));
LoginStore.loginWithPrivateKey(await PinEncrypted.create(hexKey, pin));
return; return;
} else { } else {
throw new Error("INVALID PRIVATE KEY"); throw new Error("INVALID PRIVATE KEY");
@ -36,17 +33,15 @@ export default function useLoginHandler() {
if (!hasSubtleCrypto) { if (!hasSubtleCrypto) {
throw new Error(insecureMsg); throw new Error(insecureMsg);
} }
if (!pin) throw new PinRequiredError();
const ent = generateBip39Entropy(key); const ent = generateBip39Entropy(key);
const keyHex = entropyToPrivateKey(ent); const hexKey = entropyToPrivateKey(ent);
LoginStore.loginWithPrivateKey(await PinEncrypted.create(keyHex, pin)); LoginStore.loginWithPrivateKey(await pin(hexKey));
return; return;
} else if (key.length === 64) { } else if (key.length === 64) {
if (!hasSubtleCrypto) { if (!hasSubtleCrypto) {
throw new Error(insecureMsg); throw new Error(insecureMsg);
} }
if (!pin) throw new PinRequiredError(); LoginStore.loginWithPrivateKey(await pin(key));
LoginStore.loginWithPrivateKey(await PinEncrypted.create(key, pin));
return; return;
} }
@ -58,7 +53,6 @@ export default function useLoginHandler() {
const hexKey = await getNip05PubKey(key); const hexKey = await getNip05PubKey(key);
LoginStore.loginWithPubkey(hexKey, LoginSessionType.PublicKey); LoginStore.loginWithPubkey(hexKey, LoginSessionType.PublicKey);
} else if (key.startsWith("bunker://")) { } else if (key.startsWith("bunker://")) {
if (!pin) throw new PinRequiredError();
const nip46 = new Nip46Signer(key); const nip46 = new Nip46Signer(key);
await nip46.init(); await nip46.init();
@ -68,7 +62,7 @@ export default function useLoginHandler() {
LoginSessionType.Nip46, LoginSessionType.Nip46,
undefined, undefined,
nip46.relays, nip46.relays,
await PinEncrypted.create(unwrap(nip46.privateKey), pin), await pin(unwrap(nip46.privateKey)),
); );
nip46.close(); nip46.close();
} else { } 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 { unixNowMs } from "@snort/shared";
import * as secp from "@noble/curves/secp256k1"; import * as secp from "@noble/curves/secp256k1";
import * as utils from "@noble/curves/abstract/utils"; import * as utils from "@noble/curves/abstract/utils";
@ -10,7 +10,6 @@ import { bech32ToHex, dedupeById, randomSample, sanitizeRelayUrl, unwrap } from
import { SubscriptionEvent } from "Subscription"; import { SubscriptionEvent } from "Subscription";
import { System } from "index"; import { System } from "index";
import { Chats, FollowsFeed, GiftsCache, Notifications } from "Cache"; import { Chats, FollowsFeed, GiftsCache, Notifications } from "Cache";
import { PinRequiredError } from "Hooks/useLoginHandler";
import { Nip7OsSigner } from "./Nip7OsSigner"; import { Nip7OsSigner } from "./Nip7OsSigner";
export function setRelays(state: LoginSession, relays: Record<string, RelaySettings>, createdAt: number) { 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 * 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 ent = generateBip39Entropy();
const entropy = utils.bytesToHex(ent); const entropy = utils.bytesToHex(ent);
const privateKey = entropyToPrivateKey(ent); const privateKey = entropyToPrivateKey(ent);
@ -89,9 +88,7 @@ export async function generateNewLogin(pin: string) {
const publisher = EventPublisher.privateKey(privateKey); const publisher = EventPublisher.privateKey(privateKey);
const ev = await publisher.contactList([bech32ToHex(SnortPubKey), publicKey], newRelays); const ev = await publisher.contactList([bech32ToHex(SnortPubKey), publicKey], newRelays);
System.BroadcastEvent(ev); System.BroadcastEvent(ev);
LoginStore.loginWithPrivateKey(await pin(privateKey), entropy, newRelays);
const key = await PinEncrypted.create(privateKey, pin);
LoginStore.loginWithPrivateKey(key, entropy, newRelays);
} }
export function generateRandomKey() { export function generateRandomKey() {
@ -175,22 +172,17 @@ export function addSubscription(state: LoginSession, ...subs: SubscriptionEvent[
} }
export function sessionNeedsPin(l: LoginSession) { 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) { switch (l.type) {
case LoginSessionType.PrivateKey: { case LoginSessionType.PrivateKey: {
if (!pin) throw new PinRequiredError(); return EventPublisher.privateKey(unwrap(l.privateKeyData as KeyStorage).value);
l.privateKeyData = pin;
return EventPublisher.privateKey(pin.value);
} }
case LoginSessionType.Nip46: { case LoginSessionType.Nip46: {
if (!pin) throw new PinRequiredError();
l.privateKeyData = pin;
const relayArgs = (l.remoteSignerRelays ?? []).map(a => `relay=${encodeURIComponent(a)}`); 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); const nip46 = new Nip46Signer(`bunker://${unwrap(l.publicKey)}?${[...relayArgs].join("&")}`, inner);
return new EventPublisher(nip46, unwrap(l.publicKey)); 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 { UserPreferences } from "Login";
import { SubscriptionEvent } from "Subscription"; import { SubscriptionEvent } from "Subscription";
@ -47,7 +47,7 @@ export interface LoginSession {
/** /**
* Encrypted private key * Encrypted private key
*/ */
privateKeyData?: PinEncrypted | PinEncryptedPayload; privateKeyData?: KeyStorage;
/** /**
* BIP39-generated, hex-encoded entropy * 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 * as utils from "@noble/curves/abstract/utils";
import { v4 as uuid } from "uuid"; 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 { deepClone, sanitizeRelayUrl, unwrap, ExternalStore } from "@snort/shared";
import { DefaultRelays } from "Const"; import { DefaultRelays } from "Const";
@ -85,6 +85,9 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
timestamp: 0, timestamp: 0,
}; };
v.extraChats ??= []; v.extraChats ??= [];
if (v.privateKeyData) {
v.privateKeyData = KeyStorage.fromPayload(v.privateKeyData as object);
}
} }
this.#loadIrisKeyIfExists(); this.#loadIrisKeyIfExists();
} }
@ -121,7 +124,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
type: LoginSessionType, type: LoginSessionType,
relays?: Record<string, RelaySettings>, relays?: Record<string, RelaySettings>,
remoteSignerRelays?: Array<string>, remoteSignerRelays?: Array<string>,
privateKey?: PinEncrypted, privateKey?: KeyStorage,
) { ) {
if (this.#accounts.has(key)) { if (this.#accounts.has(key)) {
throw new Error("Already logged in with this pubkey"); throw new Error("Already logged in with this pubkey");
@ -159,7 +162,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
return Object.fromEntries(DefaultRelays.entries()); 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)); const pubKey = utils.bytesToHex(secp.schnorr.getPublicKey(key.value));
if (this.#accounts.has(pubKey)) { if (this.#accounts.has(pubKey)) {
throw new Error("Already logged in with this pubkey"); throw new Error("Already logged in with this pubkey");
@ -218,13 +221,13 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
return { ...s }; return { ...s };
} }
async #loadIrisKeyIfExists() { #loadIrisKeyIfExists() {
try { try {
const irisKeyJSON = window.localStorage.getItem("iris.myKey"); const irisKeyJSON = window.localStorage.getItem("iris.myKey");
if (irisKeyJSON) { if (irisKeyJSON) {
const irisKeyObj = JSON.parse(irisKeyJSON); const irisKeyObj = JSON.parse(irisKeyJSON);
if (irisKeyObj.priv) { if (irisKeyObj.priv) {
const privateKey = await PinEncrypted.create(irisKeyObj.priv, "1234"); const privateKey = new NotEncrypted(irisKeyObj.priv);
this.loginWithPrivateKey(privateKey); this.loginWithPrivateKey(privateKey);
window.localStorage.removeItem("iris.myKey"); window.localStorage.removeItem("iris.myKey");
} }
@ -286,7 +289,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
} }
const toSave = []; const toSave = [];
for (const v of this.#accounts.values()) { for (const v of this.#accounts.values()) {
if (v.privateKeyData instanceof PinEncrypted) { if (v.privateKeyData instanceof KeyStorage) {
toSave.push({ toSave.push({
...v, ...v,
privateKeyData: v.privateKeyData.toPayload(), privateKeyData: v.privateKeyData.toPayload(),

View File

@ -3,15 +3,15 @@ import "./LoginPage.css";
import { CSSProperties, useEffect, useState } from "react"; import { CSSProperties, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useIntl, FormattedMessage } from "react-intl"; 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 ZapButton from "Element/Event/ZapButton";
import useImgProxy from "Hooks/useImgProxy"; import useImgProxy from "Hooks/useImgProxy";
import Icon from "Icons/Icon"; import Icon from "Icons/Icon";
import { generateNewLogin, LoginSessionType, LoginStore } from "Login"; import { generateNewLogin, LoginSessionType, LoginStore } from "Login";
import AsyncButton from "Element/AsyncButton"; import AsyncButton from "Element/AsyncButton";
import useLoginHandler, { PinRequiredError } from "Hooks/useLoginHandler"; import useLoginHandler from "Hooks/useLoginHandler";
import { secp256k1 } from "@noble/curves/secp256k1"; import { secp256k1 } from "@noble/curves/secp256k1";
import { bytesToHex } from "@noble/curves/abstract/utils"; import { bytesToHex } from "@noble/curves/abstract/utils";
import Modal from "Element/Modal"; import Modal from "Element/Modal";
@ -94,16 +94,20 @@ export default function LoginPage() {
setArt({ ...ret, link: url }); 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) { async function doLogin(pin?: string) {
setError(""); setError("");
try { try {
await loginHandler.doLogin(key, pin); await loginHandler.doLogin(key, key => makeKeyStore(key, pin));
navigate("/"); navigate("/");
} catch (e) { } catch (e) {
if (e instanceof PinRequiredError) {
setPin(true);
return;
}
if (e instanceof Error) { if (e instanceof Error) {
setError(e.message); setError(e.message);
} else { } else {
@ -117,9 +121,9 @@ export default function LoginPage() {
} }
} }
async function makeRandomKey(pin: string) { async function makeRandomKey(pin?: string) {
try { try {
await generateNewLogin(pin); await generateNewLogin(key => makeKeyStore(key, pin));
window.plausible?.("Generate Account"); window.plausible?.("Generate Account");
navigate("/new"); navigate("/new");
} catch (e) { } catch (e) {
@ -153,7 +157,7 @@ export default function LoginPage() {
setNip46Key(newKey); setNip46Key(newKey);
} }
async function startNip46(pin: string) { async function startNip46(pin?: string) {
if (!nostrConnect || !nip46Key) return; if (!nostrConnect || !nip46Key) return;
const signer = new Nip46Signer(nostrConnect, new PrivateKeySigner(nip46Key)); const signer = new Nip46Signer(nostrConnect, new PrivateKeySigner(nip46Key));
@ -165,7 +169,7 @@ export default function LoginPage() {
LoginSessionType.Nip46, LoginSessionType.Nip46,
undefined, undefined,
["wss://relay.damus.io"], ["wss://relay.damus.io"],
await PinEncrypted.create(nip46Key, pin), await makeKeyStore(nip46Key, pin),
); );
navigate("/"); navigate("/");
} }
@ -316,7 +320,15 @@ export default function LoginPage() {
/> />
</p> </p>
<div dir="auto" className="login-actions"> <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" /> <FormattedMessage defaultMessage="Login" description="Login button" />
</AsyncButton> </AsyncButton>
<AsyncButton onClick={() => setPin(true)}> <AsyncButton onClick={() => setPin(true)}>
@ -325,9 +337,22 @@ export default function LoginPage() {
{pin && ( {pin && (
<PinPrompt <PinPrompt
subTitle={ 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 => { onResult={async pin => {
setPin(false); setPin(false);
@ -339,7 +364,16 @@ export default function LoginPage() {
await makeRandomKey(pin); await makeRandomKey(pin);
} }
}} }}
onCancel={() => setPin(false)} onCancel={async () => {
setPin(false);
if (key) {
await doLogin();
} else if (nostrConnect) {
await startNip46();
} else {
await makeRandomKey();
}
}}
/> />
)} )}
{altLogins()} {altLogins()}

View File

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

View File

@ -3,3 +3,28 @@
border: 2px dashed #222222; border: 2px dashed #222222;
border-radius: 16px; 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 "./Keys.css";
import FormattedMessage from "Element/FormattedMessage"; 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 Copy from "Element/Copy";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
@ -11,25 +11,34 @@ export default function ExportKeys() {
const { publicKey, privateKeyData, generatedEntropy } = useLogin(); const { publicKey, privateKeyData, generatedEntropy } = useLogin();
return ( return (
<div className="flex-column g12"> <div className="flex-column g12">
<h3> <h2>
<FormattedMessage defaultMessage="Public Key" /> <FormattedMessage defaultMessage="Public Key" />
</h3> </h2>
<Copy text={hexToBech32("npub", publicKey ?? "")} className="dashed" /> <Copy text={hexToBech32("npub", publicKey ?? "")} className="dashed" />
<Copy text={encodeTLV(NostrPrefix.Profile, publicKey ?? "")} className="dashed" /> <Copy text={encodeTLV(NostrPrefix.Profile, publicKey ?? "")} className="dashed" />
{privateKeyData instanceof PinEncrypted && ( {privateKeyData instanceof KeyStorage && (
<> <>
<h3> <h2>
<FormattedMessage defaultMessage="Private Key" /> <FormattedMessage defaultMessage="Private Key" />
</h3> </h2>
<Copy text={hexToBech32("nsec", privateKeyData.value)} className="dashed" /> <Copy text={hexToBech32("nsec", privateKeyData.value)} className="dashed" />
</> </>
)} )}
{generatedEntropy && ( {generatedEntropy && (
<> <>
<h3> <h2>
<FormattedMessage defaultMessage="Mnemonic" /> <FormattedMessage defaultMessage="Mnemonic" />
</h3> </h2>
<Copy text={hexToMnemonic(generatedEntropy ?? "")} className="dashed" /> <div className="mnemonic-grid">
{hexToMnemonic(generatedEntropy ?? "")
.split(" ")
.map((a, i) => (
<div className="flex word">
<div>{i + 1}</div>
<div>{a}</div>
</div>
))}
</div>
</> </>
)} )}
</div> </div>

View File

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

View File

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