Adding QR codes for better air-gaped experience

This commit is contained in:
Luis Miguel García Mancebo
2023-02-26 21:33:54 +01:00
committed by fiatjaf_
parent 9302797132
commit 72c9e516e3
5 changed files with 389 additions and 216 deletions

View File

@@ -1,7 +1,8 @@
import browser from 'webextension-polyfill'
import React, {useState, useCallback, useEffect} from 'react'
import {render} from 'react-dom'
import {generatePrivateKey, nip19} from 'nostr-tools'
import {generatePrivateKey, getPublicKey, nip19} from 'nostr-tools'
import QRCode from 'react-qr-code'
import {
getPermissionsString,
@@ -10,13 +11,15 @@ import {
} from './common'
function Options() {
let [key, setKey] = useState('')
let [pubKey, setPubKey] = useState('')
let [privKey, setPrivKey] = useState('')
let [relays, setRelays] = useState([])
let [newRelayURL, setNewRelayURL] = useState('')
let [permissions, setPermissions] = useState()
let [protocolHandler, setProtocolHandler] = useState(null)
let [hidingPrivateKey, hidePrivateKey] = useState(true)
let [message, setMessage] = useState('')
let [showQR, setShowQR] = useState('')
const showMessage = useCallback(msg => {
setMessage(msg)
@@ -25,23 +28,30 @@ function Options() {
useEffect(() => {
browser.storage.local
.get(['private_key', 'relays', 'protocol_handler'])
.then(results => {
if (results.private_key) setKey(nip19.nsecEncode(results.private_key))
if (results.relays) {
let relaysList = []
for (let url in results.relays) {
relaysList.push({
url,
policy: results.relays[url]
})
}
setRelays(relaysList)
.get(['private_key', 'relays', 'protocol_handler'])
.then(results => {
if (results.private_key) {
setPrivKey(nip19.nsecEncode(results.private_key))
let hexKey = getPublicKey(results.private_key)
let npubKey = nip19.npubEncode(hexKey)
setPubKey(npubKey)
}
if (results.relays) {
let relaysList = []
for (let url in results.relays) {
relaysList.push({
url,
policy: results.relays[url]
})
}
if (results.protocol_handler) {
setProtocolHandler(results.protocol_handler)
}
})
setRelays(relaysList)
}
if (results.protocol_handler) {
setProtocolHandler(results.protocol_handler)
}
})
}, [])
useEffect(() => {
@@ -51,164 +61,184 @@ function Options() {
function loadPermissions() {
readPermissions().then(permissions => {
setPermissions(
Object.entries(permissions).map(
([host, {level, condition, created_at}]) => ({
host,
level,
condition,
created_at
})
)
Object.entries(permissions).map(
([host, {level, condition, created_at}]) => ({
host,
level,
condition,
created_at
})
)
)
})
}
return (
<>
<h1>nos2x</h1>
<p>nostr signer extension</p>
<h2>options</h2>
<div style={{marginBottom: '10px'}}>
<div style={{display: 'flex', alignItems: 'center'}}>
<span>preferred relays:</span>
<button style={{marginLeft: '20px'}} onClick={saveRelays}>
save
</button>
</div>
<div style={{marginLeft: '10px'}}>
{relays.map(({url, policy}, i) => (
<div key={i} style={{display: 'flex'}}>
<input
style={{marginRight: '10px', width: '400px'}}
value={url}
onChange={changeRelayURL.bind(null, i)}
/>
<label>
read
<input
type="checkbox"
checked={policy.read}
onChange={toggleRelayPolicy.bind(null, i, 'read')}
/>
</label>
<label>
write
<input
type="checkbox"
checked={policy.write}
onChange={toggleRelayPolicy.bind(null, i, 'write')}
/>
</label>
</div>
))}
<div style={{display: 'flex'}}>
<input
style={{width: '400px'}}
value={newRelayURL}
onChange={e => setNewRelayURL(e.target.value)}
onBlur={addNewRelay}
/>
</div>
</div>
</div>
<div style={{marginBottom: '10px'}}>
<label>
<div>private key:&nbsp;</div>
<div style={{marginLeft: '10px'}}>
<div style={{display: 'flex'}}>
<input
type={hidingPrivateKey ? 'password' : 'text'}
style={{width: '600px'}}
value={key}
onChange={handleKeyChange}
onFocus={() => hidePrivateKey(false)}
onBlur={() => hidePrivateKey(true)}
/>
{key === '' && <button onClick={generate}>generate</button>}
</div>
<button disabled={!isKeyValid()} onClick={saveKey}>
<>
<h1>nos2x</h1>
<p>nostr signer extension</p>
<h2>options</h2>
<div style={{marginBottom: '10px'}}>
<div style={{display: 'flex', alignItems: 'center'}}>
<span>preferred relays:</span>
<button style={{marginLeft: '20px'}} onClick={saveRelays}>
save
</button>
</div>
</label>
{permissions?.length > 0 && (
<>
<h2>permissions</h2>
<table>
<thead>
<tr>
<th>domain</th>
<th>permissions</th>
<th>condition</th>
<th>since</th>
<th></th>
</tr>
</thead>
<tbody>
{permissions.map(({host, level, condition, created_at}) => (
<tr key={host}>
<td>{host}</td>
<td>{getPermissionsString(level)}</td>
<td>{condition}</td>
<td>
{new Date(created_at * 1000)
.toISOString()
.split('.')[0]
.split('T')
.join(' ')}
</td>
<td>
<button onClick={handleRevoke} data-domain={host}>
revoke
</button>
</td>
<div style={{marginLeft: '10px'}}>
{relays.map(({url, policy}, i) => (
<div key={i} style={{display: 'flex'}}>
<input
style={{marginRight: '10px', width: '400px'}}
value={url}
onChange={changeRelayURL.bind(null, i)}
/>
<label>
read
<input
type="checkbox"
checked={policy.read}
onChange={toggleRelayPolicy.bind(null, i, 'read')}
/>
</label>
<label>
write
<input
type="checkbox"
checked={policy.write}
onChange={toggleRelayPolicy.bind(null, i, 'write')}
/>
</label>
</div>
))}
<div style={{display: 'flex'}}>
<input
style={{width: '400px'}}
value={newRelayURL}
onChange={e => setNewRelayURL(e.target.value)}
onBlur={addNewRelay}
/>
</div>
</div>
</div>
<div style={{marginBottom: '10px'}}>
<label>
<div>private key:&nbsp;</div>
<div style={{marginLeft: '10px'}}>
<div style={{display: 'flex'}}>
<input
type={hidingPrivateKey ? 'password' : 'text'}
style={{width: '600px'}}
value={privKey}
onChange={handleKeyChange}
onFocus={() => hidePrivateKey(false)}
onBlur={() => hidePrivateKey(true)}
/>
{privKey === '' && <button onClick={generate}>generate</button>}
</div>
<button disabled={!isKeyValid()} onClick={saveKey}>
save
</button>
<button disabled={!isKeyValid()} onClick={() => setShowQR('priv')}>
Show QR for private key
</button>
<button disabled={!isKeyValid()} onClick={() => setShowQR('pub')}>
Show QR for public key
</button>
{ showQR && (
<div id={'qrCodeDiv'} style={{ height: 'auto', maxWidth: 256, width: '100%', marginTop: '20px', marginBottom: '30px' }}>
<QRCode
size={256}
style={{ height: 'auto', maxWidth: '100%', width: '100%' }}
value={showQR === 'priv' ? privKey : pubKey}
viewBox={`0 0 256 256`}
/>
</div>
)}
</div>
</label>
{permissions?.length > 0 && (
<>
<h2>permissions</h2>
<table>
<thead>
<tr>
<th>domain</th>
<th>permissions</th>
<th>condition</th>
<th>since</th>
<th></th>
</tr>
))}
</tbody>
</table>
</>
)}
</div>
<div>
<h2>
handle{' '}
<span style={{padding: '2px', background: 'silver'}}>nostr:</span>{' '}
links:
</h2>
<div style={{marginLeft: '10px'}}>
<div>
<label>
<input
type="radio"
name="ph"
value="no"
checked={protocolHandler === null}
onChange={handleChangeProtocolHandler}
/>{' '}
no
</label>
</div>
<div>
<label>
<input
type="radio"
name="ph"
value="yes"
checked={protocolHandler !== null}
onChange={handleChangeProtocolHandler}
/>
yes
</label>
</div>
{protocolHandler !== null && (
</thead>
<tbody>
{permissions.map(({host, level, condition, created_at}) => (
<tr key={host}>
<td>{host}</td>
<td>{getPermissionsString(level)}</td>
<td>{condition}</td>
<td>
{new Date(created_at * 1000)
.toISOString()
.split('.')[0]
.split('T')
.join(' ')}
</td>
<td>
<button onClick={handleRevoke} data-domain={host}>
revoke
</button>
</td>
</tr>
))}
</tbody>
</table>
</>
)}
</div>
<div>
<h2>
handle{' '}
<span style={{padding: '2px', background: 'silver'}}>nostr:</span>{' '}
links:
</h2>
<div style={{marginLeft: '10px'}}>
<div>
<input
placeholder="url template"
value={protocolHandler}
onChange={handleChangeProtocolHandler}
style={{width: '680px', maxWidth: '90%'}}
/>
<pre>{`
<label>
<input
type="radio"
name="ph"
value="no"
checked={protocolHandler === null}
onChange={handleChangeProtocolHandler}
/>{' '}
no
</label>
</div>
<div>
<label>
<input
type="radio"
name="ph"
value="yes"
checked={protocolHandler !== null}
onChange={handleChangeProtocolHandler}
/>
yes
</label>
</div>
{protocolHandler !== null && (
<div>
<input
placeholder="url template"
value={protocolHandler}
onChange={handleChangeProtocolHandler}
style={{width: '680px', maxWidth: '90%'}}
/>
<pre>{`
{hex} = hex pubkey for npub or nprofile, hex event id for note or nevent
{p_or_e} = "p" for npub or nprofile, "e" for note or nevent
{u_or_n} = "u" for npub or nprofile, "n" for note or nevent
@@ -223,36 +253,36 @@ function Options() {
- https://brb.io/{u_or_n}/{hex}
- https://notes.blockcore.net/{p_or_e}/{hex}
`}</pre>
</div>
)}
<button
style={{marginTop: '10px'}}
onClick={saveNostrProtocolHandlerSettings}
>
save
</button>
</div>
)}
<button
style={{marginTop: '10px'}}
onClick={saveNostrProtocolHandlerSettings}
>
save
</button>
</div>
</div>
</div>
<div style={{marginTop: '12px', fontSize: '120%'}}>{message}</div>
</>
<div style={{marginTop: '12px', fontSize: '120%'}}>{message}</div>
</>
)
async function handleKeyChange(e) {
let key = e.target.value.toLowerCase().trim()
setKey(key)
setPrivKey(key)
}
async function generate(e) {
setKey(nip19.nsecEncode(generatePrivateKey()))
async function generate() {
setPrivKey(nip19.nsecEncode(generatePrivateKey()))
}
async function saveKey() {
if (!isKeyValid()) return
let hexOrEmptyKey = key
let hexOrEmptyKey = privKey
try {
let {type, data} = nip19.decode(key)
let {type, data} = nip19.decode(privKey)
if (type === 'nsec') hexOrEmptyKey = data
} catch (_) {}
@@ -261,17 +291,17 @@ function Options() {
})
if (hexOrEmptyKey !== '') {
setKey(nip19.nsecEncode(hexOrEmptyKey))
setPrivKey(nip19.nsecEncode(hexOrEmptyKey))
}
showMessage('saved private key!')
}
function isKeyValid() {
if (key === '') return true
if (key.match(/^[a-f0-9]{64}$/)) return true
if (privKey === '') return true
if (privKey.match(/^[a-f0-9]{64}$/)) return true
try {
if (nip19.decode(key).type === 'nsec') return true
if (nip19.decode(privKey).type === 'nsec') return true
} catch (_) {}
return false
}
@@ -316,7 +346,7 @@ function Options() {
async function saveRelays() {
await browser.storage.local.set({
relays: Object.fromEntries(
relays
relays
.filter(({url}) => url.trim() !== '')
.map(({url, policy}) => [url.trim(), policy])
)