NIP-05 Login

This commit is contained in:
Kieran 2023-01-02 23:36:30 +00:00
parent 0d01e02f42
commit 98262463c4
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
7 changed files with 69 additions and 59 deletions

View File

@ -23,8 +23,8 @@ export function NoteCreator(props) {
return ( return (
<> <>
{replyTo ? <small>{`Reply to: ${replyTo.Id.substring(0, 8)}`}</small> : null} {replyTo ? <small>{`Reply to: ${replyTo.Id.substring(0, 8)}`}</small> : null}
<div className="send-note"> <div className="flex">
<input type="text" placeholder="Sup?" value={note} onChange={(e) => setNote(e.target.value)}></input> <input type="text" placeholder="Sup?" value={note} onChange={(e) => setNote(e.target.value)} className="f-grow mr10"></input>
<div className="btn" onClick={() => sendNote()}>Send</div> <div className="btn" onClick={() => sendNote()}>Send</div>
</div> </div>
</> </>

View File

@ -22,7 +22,6 @@ export default function useEventPublisher() {
if (nip07 === true && hasNip07) { if (nip07 === true && hasNip07) {
ev.Id = await ev.CreateId(); ev.Id = await ev.CreateId();
let tmpEv = await window.nostr.signEvent(ev.ToObject()); let tmpEv = await window.nostr.signEvent(ev.ToObject());
console.log(tmpEv);
return Event.FromObject(tmpEv); return Event.FromObject(tmpEv);
} else { } else {
await ev.Sign(privKey); await ev.Sign(privKey);

View File

@ -172,4 +172,8 @@ body.scroll-lock {
.tabs > div { .tabs > div {
margin-right: 10px; margin-right: 10px;
}
.error {
color: red;
} }

View File

@ -4,13 +4,16 @@ import { useNavigate } from "react-router-dom";
import * as secp from '@noble/secp256k1'; import * as secp from '@noble/secp256k1';
import { bech32 } from "bech32"; import { bech32 } from "bech32";
import { setPrivateKey, setNip07PubKey } from "../state/Login"; import { setPrivateKey, setPublicKey } from "../state/Login";
const EmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
export default function LoginPage() { export default function LoginPage() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const publicKey = useSelector(s => s.login.publicKey); const publicKey = useSelector(s => s.login.publicKey);
const [key, setKey] = useState(""); const [key, setKey] = useState("");
const [error, setError] = useState("");
useEffect(() => { useEffect(() => {
if (publicKey) { if (publicKey) {
@ -18,22 +21,51 @@ export default function LoginPage() {
} }
}, [publicKey]); }, [publicKey]);
function doLogin() { function bech32ToHex(str) {
if (key.startsWith("nsec")) { let nKey = bech32.decode(str);
let nKey = bech32.decode(key); let buff = bech32.fromWords(nKey.words);
let buff = bech32.fromWords(nKey.words); return secp.utils.bytesToHex(Uint8Array.from(buff));
let hexKey = secp.utils.bytesToHex(Uint8Array.from(buff)); }
if (secp.utils.isValidPrivateKey(hexKey)) {
dispatch(setPrivateKey(hexKey)); async function getNip05PubKey(addr) {
} else { let [username, domain] = addr.split("@");
throw "INVALID PRIVATE KEY"; let rsp = await fetch(`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(username)}`);
if (rsp.ok) {
let data = await rsp.json();
let pKey = data.names[username];
if (pKey) {
return pKey;
} }
} else { }
if (secp.utils.isValidPrivateKey(key)) { throw "User key not found"
dispatch(setPrivateKey(key)); }
async function doLogin() {
try {
if (key.startsWith("nsec")) {
let hexKey = bech32ToHex(key);
if (secp.utils.isValidPrivateKey(hexKey)) {
dispatch(setPrivateKey(hexKey));
} else {
throw "INVALID PRIVATE KEY";
}
} else if (key.startsWith("npub")) {
let hexKey = bech32ToHex(key);
dispatch(setPublicKey(hexKey));
} else if (key.match(EmailRegex)) {
let hexKey = await getNip05PubKey(key);
dispatch(setPublicKey(hexKey));
} else { } else {
throw "INVALID PRIVATE KEY"; if (secp.utils.isValidPrivateKey(key)) {
dispatch(setPrivateKey(key));
} else {
throw "INVALID PRIVATE KEY";
}
} }
} catch (e) {
setError(`Failed to load NIP-05 pub key (${e})`);
console.error(e);
} }
} }
@ -45,7 +77,7 @@ export default function LoginPage() {
async function doNip07Login() { async function doNip07Login() {
let pubKey = await window.nostr.getPublicKey(); let pubKey = await window.nostr.getPublicKey();
dispatch(setNip07PubKey(pubKey)); dispatch(setPublicKey(pubKey));
} }
function altLogins() { function altLogins() {
@ -67,10 +99,10 @@ export default function LoginPage() {
return ( return (
<> <>
<h1>Login</h1> <h1>Login</h1>
<p>Enter your private key:</p>
<div className="flex"> <div className="flex">
<input type="text" placeholder="Private key" className="f-grow" onChange={e => setKey(e.target.value)} /> <input type="text" placeholder="nsec / npub / nip-05 / hex private key..." className="f-grow" onChange={e => setKey(e.target.value)} />
</div> </div>
{error.length > 0 ? <b className="error">{error}</b> : null}
<div className="tabs"> <div className="tabs">
<div className="btn" onClick={(e) => doLogin()}>Login</div> <div className="btn" onClick={(e) => doLogin()}>Login</div>
<div className="btn" onClick={() => makeRandomKey()}>Generate Key</div> <div className="btn" onClick={() => makeRandomKey()}>Generate Key</div>

View File

@ -1,16 +0,0 @@
.send-note {
display: flex;
margin-bottom: 10px;
}
.send-note > input[type="text"] {
flex-grow: 1;
}
.send-note > .btn {
margin: 0 5px;
}
.login {
margin-bottom: 10px;
}

View File

@ -1,4 +1,3 @@
import "./Root.css";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import Note from "../element/Note"; import Note from "../element/Note";
import useTimelineFeed from "../feed/TimelineFeed"; import useTimelineFeed from "../feed/TimelineFeed";
@ -19,9 +18,7 @@ export default function RootPage() {
<> <>
{pubKey ? <NoteCreator /> : null} {pubKey ? <NoteCreator /> : null}
{followHints()} {followHints()}
<div className="timeline"> {notes?.sort((a, b) => b.created_at - a.created_at).map(e => <Note key={e.id} data={e} />)}
{notes?.sort((a, b) => b.created_at - a.created_at).map(e => <Note key={e.id} data={e} />)}
</div>
</> </>
); );
} }

View File

@ -2,7 +2,7 @@ import { createSlice } from '@reduxjs/toolkit'
import * as secp from '@noble/secp256k1'; import * as secp from '@noble/secp256k1';
const PrivateKeyItem = "secret"; const PrivateKeyItem = "secret";
const Nip07PublicKeyItem = "nip07:pubkey"; const PublicKeyItem = "pubkey";
const NotificationsReadItem = "notifications-read"; const NotificationsReadItem = "notifications-read";
const LoginSlice = createSlice({ const LoginSlice = createSlice({
@ -33,11 +33,6 @@ const LoginSlice = createSlice({
*/ */
follows: [], follows: [],
/**
* Login keys are managed by extension
*/
nip07: false,
/** /**
* Notifications for this login session * Notifications for this login session
*/ */
@ -52,7 +47,7 @@ const LoginSlice = createSlice({
init: (state) => { init: (state) => {
state.privateKey = window.localStorage.getItem(PrivateKeyItem); state.privateKey = window.localStorage.getItem(PrivateKeyItem);
if (state.privateKey) { if (state.privateKey) {
window.localStorage.removeItem(Nip07PublicKeyItem); // reset nip07 if using private key window.localStorage.removeItem(PublicKeyItem); // reset nip07 if using private key
state.publicKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(state.privateKey, true)); state.publicKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(state.privateKey, true));
state.loggedOut = false; state.loggedOut = false;
} else { } else {
@ -65,11 +60,10 @@ const LoginSlice = createSlice({
"wss://nostr-pub.wellorder.net": { read: true, write: true } "wss://nostr-pub.wellorder.net": { read: true, write: true }
}; };
// check nip07 pub key // check pub key only
let nip07PubKey = window.localStorage.getItem(Nip07PublicKeyItem); let pubKey = window.localStorage.getItem(PublicKeyItem);
if (nip07PubKey && !state.privateKey) { if (pubKey && !state.privateKey) {
state.publicKey = nip07PubKey; state.publicKey = pubKey;
state.nip07 = true;
state.loggedOut = false; state.loggedOut = false;
} }
@ -80,18 +74,16 @@ const LoginSlice = createSlice({
} }
}, },
setPrivateKey: (state, action) => { setPrivateKey: (state, action) => {
state.loggedOut = false;
state.privateKey = action.payload; state.privateKey = action.payload;
window.localStorage.setItem(PrivateKeyItem, action.payload); window.localStorage.setItem(PrivateKeyItem, action.payload);
state.publicKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(action.payload, true)); state.publicKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(action.payload, true));
}, },
setPublicKey: (state, action) => { setPublicKey: (state, action) => {
window.localStorage.setItem(PublicKeyItem, action.payload);
state.loggedOut = false;
state.publicKey = action.payload; state.publicKey = action.payload;
}, },
setNip07PubKey: (state, action) => {
window.localStorage.setItem(Nip07PublicKeyItem, action.payload);
state.publicKey = action.payload;
state.nip07 = true;
},
setRelays: (state, action) => { setRelays: (state, action) => {
// filter out non-websocket urls // filter out non-websocket urls
let filtered = Object.entries(action.payload) let filtered = Object.entries(action.payload)
@ -119,12 +111,14 @@ const LoginSlice = createSlice({
}, },
logout: (state) => { logout: (state) => {
window.localStorage.removeItem(PrivateKeyItem); window.localStorage.removeItem(PrivateKeyItem);
window.localStorage.removeItem(Nip07PublicKeyItem); window.localStorage.removeItem(PublicKeyItem);
window.localStorage.removeItem(NotificationsReadItem);
state.privateKey = null; state.privateKey = null;
state.publicKey = null; state.publicKey = null;
state.follows = []; state.follows = [];
state.notifications = []; state.notifications = [];
state.loggedOut = true; state.loggedOut = true;
state.readNotifications = 0;
}, },
markNotificationsRead: (state) => { markNotificationsRead: (state) => {
state.readNotifications = new Date().getTime(); state.readNotifications = new Date().getTime();
@ -133,5 +127,5 @@ const LoginSlice = createSlice({
} }
}); });
export const { init, setPrivateKey, setPublicKey, setNip07PubKey, setRelays, setFollows, addNotifications, logout, markNotificationsRead } = LoginSlice.actions; export const { init, setPrivateKey, setPublicKey, setRelays, setFollows, addNotifications, logout, markNotificationsRead } = LoginSlice.actions;
export const reducer = LoginSlice.reducer; export const reducer = LoginSlice.reducer;