Adding QR codes for better air-gaped experience
This commit is contained in:
committed by
fiatjaf_
parent
9302797132
commit
72c9e516e3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "nos2x",
|
||||
"description": "Nostr Signer Extension",
|
||||
"version": "1.8.1",
|
||||
"version": "1.8.2",
|
||||
"homepage_url": "https://github.com/fiatjaf/nos2x",
|
||||
"manifest_version": 3,
|
||||
"icons": {
|
||||
|
||||
@@ -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: </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: </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])
|
||||
)
|
||||
|
||||
@@ -2,18 +2,33 @@ import browser from 'webextension-polyfill'
|
||||
import {render} from 'react-dom'
|
||||
import {getPublicKey, nip19} from 'nostr-tools'
|
||||
import React, {useState, useRef, useEffect} from 'react'
|
||||
import QRCode from 'react-qr-code'
|
||||
|
||||
function Popup() {
|
||||
let [key, setKey] = useState('')
|
||||
let [pubKey, setPubKey] = useState('')
|
||||
let [privKey, setPrivKey] = useState('')
|
||||
let keys = useRef([])
|
||||
let [showQR, setShowQR] = useState('')
|
||||
|
||||
const QrIcon = () => (
|
||||
<svg width="30px" height="30px" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" className="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M6.75 6.75h.75v.75h-.75v-.75zM6.75 16.5h.75v.75h-.75v-.75zM16.5 6.75h.75v.75h-.75v-.75zM13.5 13.5h.75v.75h-.75v-.75zM13.5 19.5h.75v.75h-.75v-.75zM19.5 13.5h.75v.75h-.75v-.75zM19.5 19.5h.75v.75h-.75v-.75zM16.5 16.5h.75v.75h-.75v-.75z"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
browser.storage.local.get(['private_key', 'relays']).then(results => {
|
||||
setPrivKey(results.private_key)
|
||||
|
||||
if (results.private_key) {
|
||||
let hexKey = getPublicKey(results.private_key)
|
||||
let npubKey = nip19.npubEncode(hexKey)
|
||||
|
||||
setKey(npubKey)
|
||||
setPubKey(npubKey)
|
||||
|
||||
keys.current.push(npubKey)
|
||||
keys.current.push(hexKey)
|
||||
@@ -35,42 +50,66 @@ function Popup() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setKey(null)
|
||||
setPubKey(null)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>nos2x</h2>
|
||||
{key === null ? (
|
||||
<p style={{width: '150px'}}>
|
||||
you don't have a private key set. use the options page to set one.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
<a onClick={toggleKeyType}>↩️</a> your public key:
|
||||
</p>
|
||||
<pre
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
width: '100px'
|
||||
}}
|
||||
>
|
||||
<code>{key}</code>
|
||||
<>
|
||||
<h2>nos2x</h2>
|
||||
{pubKey === null ? (
|
||||
<p style={{width: '150px'}}>
|
||||
you don't have a private key set. use the options page to set one.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
<a onClick={toggleKeyType}>↩️</a> your public key:
|
||||
</p>
|
||||
<pre
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
width: '200px'
|
||||
}}
|
||||
>
|
||||
<code>{pubKey}</code>
|
||||
</pre>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
<div style={{float: 'left', marginRight: '30px', marginBottom: '20px'}}>
|
||||
<a onClick={() => setShowQR('pub')}>
|
||||
<QrIcon></QrIcon> PUB
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div style={{float: 'left', marginRight: '30px', marginBottom: '20px'}}>
|
||||
<a onClick={() => setShowQR('priv')}>
|
||||
<QrIcon></QrIcon> PRIV
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{ showQR && (
|
||||
<div id={'qrCodeDiv'} style={{ height: 'auto', margin: '0 auto', maxWidth: 256, width: '100%', marginTop: '50px' }}>
|
||||
{showQR === 'priv' ? (<p>PRIVATE KEY</p>) : (<p>PUBLIC KEY</p>)}
|
||||
<QRCode
|
||||
size={256}
|
||||
style={{ height: 'auto', maxWidth: '100%', width: '100%' }}
|
||||
value={showQR === 'priv' ? privKey : pubKey}
|
||||
viewBox={`0 0 256 256`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
function toggleKeyType(e) {
|
||||
e.preventDefault()
|
||||
let nextKeyType =
|
||||
keys.current[(keys.current.indexOf(key) + 1) % keys.current.length]
|
||||
setKey(nextKeyType)
|
||||
keys.current[(keys.current.indexOf(pubKey) + 1) % keys.current.length]
|
||||
setPubKey(nextKeyType)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user