Compare commits
10 Commits
main
...
feature/ad
Author | SHA1 | Date | |
---|---|---|---|
|
5ca798958f | ||
|
33b088383d | ||
|
fe462376c6 | ||
|
a9b4b22c34 | ||
|
3c2d2f9f84 | ||
|
cbd16d1e35 | ||
|
88f559b8f1 | ||
|
c977b96eae | ||
|
17c1c13ad7 | ||
|
b8a57c33d6 |
@ -10,7 +10,7 @@ import { isEmptyString } from '@/utils/helpers/helpers'
|
|||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
|
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
|
||||||
import { selectApps } from '@/store'
|
import { selectApps } from '@/store'
|
||||||
import { dbi } from '@/modules/db'
|
import { DbApp, dbi } from '@/modules/db'
|
||||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||||
import { setApps } from '@/store/reducers/content.slice'
|
import { setApps } from '@/store/reducers/content.slice'
|
||||||
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
||||||
@ -20,7 +20,7 @@ export const ModalAppDetails = () => {
|
|||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.APP_DETAILS)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.APP_DETAILS)
|
||||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.APP_DETAILS)
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.APP_DETAILS)
|
||||||
|
|
||||||
const { appNpub = '' } = useParams()
|
const { npub = '', appNpub = '' } = useParams()
|
||||||
const apps = useAppSelector(selectApps)
|
const apps = useAppSelector(selectApps)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
@ -33,8 +33,8 @@ export const ModalAppDetails = () => {
|
|||||||
})
|
})
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
const currentApp = apps.find((app) => app.appNpub === appNpub && app.npub === npub)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentApp = apps.find((app) => app.appNpub === appNpub)
|
|
||||||
if (!currentApp) return
|
if (!currentApp) return
|
||||||
|
|
||||||
setDetails({
|
setDetails({
|
||||||
@ -94,14 +94,15 @@ export const ModalAppDetails = () => {
|
|||||||
|
|
||||||
const submitHandler = async (e: FormEvent) => {
|
const submitHandler = async (e: FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (isLoading) return undefined
|
if (isLoading || !currentApp) return undefined
|
||||||
try {
|
try {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const updatedApp = {
|
const updatedApp: DbApp = {
|
||||||
|
...currentApp,
|
||||||
url,
|
url,
|
||||||
name,
|
name,
|
||||||
icon,
|
icon,
|
||||||
appNpub,
|
updateTimestamp: Date.now()
|
||||||
}
|
}
|
||||||
await dbi.updateApp(updatedApp)
|
await dbi.updateApp(updatedApp)
|
||||||
const apps = await dbi.listApps()
|
const apps = await dbi.listApps()
|
||||||
|
@ -2,9 +2,10 @@ import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
|||||||
import { Modal } from '@/shared/Modal/Modal'
|
import { Modal } from '@/shared/Modal/Modal'
|
||||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
import { Box, Stack, Typography } from '@mui/material'
|
import { Box, Stack, Typography } from '@mui/material'
|
||||||
import { StyledButton, StyledSettingContainer, StyledSynchedText } from './styled'
|
import { StyledButton, StyledSettingContainer, StyledSynchText, StyledSynchedText } from './styled'
|
||||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||||
import { CheckmarkIcon } from '@/assets'
|
import { CheckmarkIcon } from '@/assets'
|
||||||
|
import GppMaybeOutlinedIcon from '@mui/icons-material/GppMaybeOutlined'
|
||||||
import { Input } from '@/shared/Input/Input'
|
import { Input } from '@/shared/Input/Input'
|
||||||
import { ChangeEvent, FC, useEffect, useState } from 'react'
|
import { ChangeEvent, FC, useEffect, useState } from 'react'
|
||||||
import { Checkbox } from '@/shared/Checkbox/Checkbox'
|
import { Checkbox } from '@/shared/Checkbox/Checkbox'
|
||||||
@ -17,6 +18,7 @@ import { useAppSelector } from '@/store/hooks/redux'
|
|||||||
import { selectKeys } from '@/store'
|
import { selectKeys } from '@/store'
|
||||||
import { isValidPassphase, isWeakPassphase } from '@/modules/keys'
|
import { isValidPassphase, isWeakPassphase } from '@/modules/keys'
|
||||||
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
||||||
|
import { useCopyToClipboard } from 'usehooks-ts'
|
||||||
|
|
||||||
type ModalSettingsProps = {
|
type ModalSettingsProps = {
|
||||||
isSynced: boolean
|
isSynced: boolean
|
||||||
@ -26,6 +28,7 @@ export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
|||||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
const { npub = '' } = useParams<{ npub: string }>()
|
const { npub = '' } = useParams<{ npub: string }>()
|
||||||
const keys = useAppSelector(selectKeys)
|
const keys = useAppSelector(selectKeys)
|
||||||
|
const [, copyToClipboard] = useCopyToClipboard()
|
||||||
|
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
|
|
||||||
@ -95,17 +98,37 @@ export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exportKey = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const key = (await swicCall('exportKey', npub)) as string
|
||||||
|
if (!key) notify('Specify Cloud Sync password first!', 'error')
|
||||||
|
else if (await copyToClipboard(key)) notify('Key copied to clipboard!')
|
||||||
|
else notify('Failed to copy to clipboard', 'error')
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error', error)
|
||||||
|
notify(`Failed to copy to clipboard: ${error}`, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={isModalOpened} onClose={onClose} title="Settings">
|
<Modal open={isModalOpened} onClose={onClose} title="Settings">
|
||||||
<Stack gap={'1rem'}>
|
<Stack gap={'1rem'}>
|
||||||
<StyledSettingContainer onSubmit={handleSubmit}>
|
<StyledSettingContainer onSubmit={handleSubmit}>
|
||||||
<Stack direction={'row'} justifyContent={'space-between'}>
|
<Stack direction={'row'} justifyContent={'space-between'} alignItems={'start'}>
|
||||||
<SectionTitle>Cloud sync</SectionTitle>
|
<SectionTitle>Cloud sync</SectionTitle>
|
||||||
{isSynced && (
|
{isSynced && (
|
||||||
<StyledSynchedText>
|
<StyledSynchedText>
|
||||||
<CheckmarkIcon /> Synched
|
<CheckmarkIcon /> Synched
|
||||||
</StyledSynchedText>
|
</StyledSynchedText>
|
||||||
)}
|
)}
|
||||||
|
{!isSynced && (
|
||||||
|
<StyledSynchText>
|
||||||
|
{/* <GppMaybeOutlinedIcon style={{ transform: 'scale(0.8)' }} /> */}
|
||||||
|
Not enabled
|
||||||
|
</StyledSynchText>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Box>
|
<Box>
|
||||||
<Checkbox onChange={handleChangeCheckbox} checked={isChecked} />
|
<Checkbox onChange={handleChangeCheckbox} checked={isChecked} />
|
||||||
@ -145,6 +168,18 @@ export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
|||||||
Sync {isLoading && <LoadingSpinner mode="secondary" />}
|
Sync {isLoading && <LoadingSpinner mode="secondary" />}
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
</StyledSettingContainer>
|
</StyledSettingContainer>
|
||||||
|
|
||||||
|
<StyledSettingContainer>
|
||||||
|
<Stack direction={'row'} justifyContent={'space-between'}>
|
||||||
|
<SectionTitle>Export key</SectionTitle>
|
||||||
|
</Stack>
|
||||||
|
<Typography variant="body2" color={'GrayText'}>
|
||||||
|
Export your key encrypted with your password (NIP49)
|
||||||
|
</Typography>
|
||||||
|
<StyledButton type="submit" fullWidth onClick={exportKey}>
|
||||||
|
Export
|
||||||
|
</StyledButton>
|
||||||
|
</StyledSettingContainer>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
@ -29,3 +29,11 @@ export const StyledSynchedText = styled((props: TypographyProps) => <Typography
|
|||||||
color: theme.palette.success.main,
|
color: theme.palette.success.main,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const StyledSynchText = styled((props: TypographyProps) => <Typography variant="caption" {...props} />)(({
|
||||||
|
theme,
|
||||||
|
}) => {
|
||||||
|
return {
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
@ -5,15 +5,30 @@ import NDK, {
|
|||||||
NDKEvent,
|
NDKEvent,
|
||||||
NDKNip46Backend,
|
NDKNip46Backend,
|
||||||
NDKPrivateKeySigner,
|
NDKPrivateKeySigner,
|
||||||
|
NDKRelaySet,
|
||||||
NDKSigner,
|
NDKSigner,
|
||||||
NDKSubscription,
|
NDKSubscription,
|
||||||
NDKSubscriptionCacheUsage,
|
NDKSubscriptionCacheUsage,
|
||||||
NDKUser,
|
NDKUser,
|
||||||
} from '@nostr-dev-kit/ndk'
|
} from '@nostr-dev-kit/ndk'
|
||||||
import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS, MIN_POW, MAX_POW, KIND_RPC, DOMAIN, REQ_TTL } from '../utils/consts'
|
import {
|
||||||
|
NOAUTHD_URL,
|
||||||
|
WEB_PUSH_PUBKEY,
|
||||||
|
NIP46_RELAYS,
|
||||||
|
MIN_POW,
|
||||||
|
MAX_POW,
|
||||||
|
KIND_RPC,
|
||||||
|
DOMAIN,
|
||||||
|
REQ_TTL,
|
||||||
|
KIND_DATA,
|
||||||
|
OUTBOX_RELAYS,
|
||||||
|
BROADCAST_RELAY,
|
||||||
|
APP_TAG,
|
||||||
|
} from '../utils/consts'
|
||||||
// import { Nip04 } from './nip04'
|
// import { Nip04 } from './nip04'
|
||||||
import { fetchNip05, getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers'
|
import { fetchNip05, getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers'
|
||||||
import { NostrPowEvent, minePow } from './pow'
|
import { NostrPowEvent, minePow } from './pow'
|
||||||
|
import { encrypt as encryptNip49 } from './nip49'
|
||||||
//import { PrivateKeySigner } from './signer'
|
//import { PrivateKeySigner } from './signer'
|
||||||
|
|
||||||
//const PERF_TEST = false
|
//const PERF_TEST = false
|
||||||
@ -234,8 +249,9 @@ export class NoauthBackend {
|
|||||||
private accessBuffer: DbPending[] = []
|
private accessBuffer: DbPending[] = []
|
||||||
private notifCallback: (() => void) | null = null
|
private notifCallback: (() => void) | null = null
|
||||||
private pendingNpubEvents = new Map<string, NDKEvent[]>()
|
private pendingNpubEvents = new Map<string, NDKEvent[]>()
|
||||||
|
private permSub?: NDKSubscription
|
||||||
private ndk = new NDK({
|
private ndk = new NDK({
|
||||||
explicitRelayUrls: NIP46_RELAYS,
|
explicitRelayUrls: [...NIP46_RELAYS, ...OUTBOX_RELAYS, BROADCAST_RELAY],
|
||||||
enableOutboxModel: false,
|
enableOutboxModel: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -313,8 +329,7 @@ export class NoauthBackend {
|
|||||||
// drop old pending reqs
|
// drop old pending reqs
|
||||||
const pending = await dbi.listPending()
|
const pending = await dbi.listPending()
|
||||||
for (const p of pending) {
|
for (const p of pending) {
|
||||||
if (p.timestamp < Date.now() - REQ_TTL)
|
if (p.timestamp < Date.now() - REQ_TTL) await dbi.removePending(p.id)
|
||||||
await dbi.removePending(p.id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sub = await this.swg.registration.pushManager.getSubscription()
|
const sub = await this.swg.registration.pushManager.getSubscription()
|
||||||
@ -325,6 +340,82 @@ export class NoauthBackend {
|
|||||||
// ensure we're subscribed on the server
|
// ensure we're subscribed on the server
|
||||||
if (sub) await this.sendSubscriptionToServer(k.npub, sub)
|
if (sub) await this.sendSubscriptionToServer(k.npub, sub)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this.subscribeToAppPerms()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async subscribeToAppPerms() {
|
||||||
|
if (this.permSub) {
|
||||||
|
this.permSub.stop()
|
||||||
|
this.permSub.removeAllListeners()
|
||||||
|
this.permSub = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const authors = this.keys.map((k) => nip19.decode(k.npub).data as string)
|
||||||
|
this.permSub = this.ndk.subscribe(
|
||||||
|
{
|
||||||
|
authors,
|
||||||
|
kinds: [KIND_DATA],
|
||||||
|
'#t': [APP_TAG],
|
||||||
|
limit: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
closeOnEose: false,
|
||||||
|
cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY,
|
||||||
|
},
|
||||||
|
NDKRelaySet.fromRelayUrls(OUTBOX_RELAYS, this.ndk),
|
||||||
|
true // auto-start
|
||||||
|
)
|
||||||
|
this.permSub.on('event', async (e) => {
|
||||||
|
const npub = nip19.npubEncode(e.pubkey)
|
||||||
|
const key = this.keys.find((k) => k.npub === npub)
|
||||||
|
if (!key) return
|
||||||
|
|
||||||
|
// parse
|
||||||
|
try {
|
||||||
|
const payload = await key.signer.decrypt(new NDKUser({ pubkey: e.pubkey }), e.content)
|
||||||
|
const data = JSON.parse(payload)
|
||||||
|
console.log('Got app perm event', { e, data })
|
||||||
|
// FIXME validate first!
|
||||||
|
await this.mergeAppPerms(key, data)
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Bad app perm event', e, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// notify UI
|
||||||
|
this.updateUI()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private async mergeAppPerms(key: Key, data: any) {
|
||||||
|
let app = this.apps.find(a => a.appNpub === data.appNpub)
|
||||||
|
const appFromData = (): DbApp => {
|
||||||
|
return {
|
||||||
|
npub: data.npub,
|
||||||
|
appNpub: data.appNpub,
|
||||||
|
name: data.name,
|
||||||
|
icon: data.icon,
|
||||||
|
url: data.url,
|
||||||
|
// choose older creation timestamp
|
||||||
|
timestamp: app ? Math.min(app.timestamp, data.timestamp) : data.timestamp,
|
||||||
|
updateTimestamp: data.updateTimestamp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!app) {
|
||||||
|
// new app
|
||||||
|
app = appFromData()
|
||||||
|
console.log("New app from event", { data, app })
|
||||||
|
await dbi.addApp(app)
|
||||||
|
} else if (app.updateTimestamp < data.updateTimestamp) {
|
||||||
|
// update existing app
|
||||||
|
app = appFromData()
|
||||||
|
await dbi.updateApp(app)
|
||||||
|
} else {
|
||||||
|
// old data
|
||||||
|
console.log("skip old app perms", { data, app })
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME merge perms
|
||||||
}
|
}
|
||||||
|
|
||||||
public setNotifCallback(cb: () => void) {
|
public setNotifCallback(cb: () => void) {
|
||||||
@ -653,10 +744,12 @@ export class NoauthBackend {
|
|||||||
name,
|
name,
|
||||||
nsec,
|
nsec,
|
||||||
existingName,
|
existingName,
|
||||||
|
passphrase
|
||||||
}: {
|
}: {
|
||||||
name: string
|
name: string
|
||||||
nsec?: string
|
nsec?: string
|
||||||
existingName?: boolean
|
existingName?: boolean
|
||||||
|
passphrase?: string
|
||||||
}): Promise<KeyInfo> {
|
}): Promise<KeyInfo> {
|
||||||
// lowercase
|
// lowercase
|
||||||
name = name.trim().toLocaleLowerCase()
|
name = name.trim().toLocaleLowerCase()
|
||||||
@ -674,12 +767,24 @@ export class NoauthBackend {
|
|||||||
|
|
||||||
const localKey = await this.keysModule.generateLocalKey()
|
const localKey = await this.keysModule.generateLocalKey()
|
||||||
const enckey = await this.keysModule.encryptKeyLocal(sk, localKey)
|
const enckey = await this.keysModule.encryptKeyLocal(sk, localKey)
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const dbKey: DbKey = { npub, name, enckey, localKey }
|
const dbKey: DbKey = { npub, name, enckey, localKey }
|
||||||
|
|
||||||
|
// nip49
|
||||||
|
if (passphrase)
|
||||||
|
dbKey.ncryptsec = encryptNip49(Buffer.from(sk, 'hex'), passphrase, 16, nsec ? 0x01 : 0x00)
|
||||||
|
|
||||||
|
// FIXME this is all one big complex TX and if something fails
|
||||||
|
// we have to gracefully proceed somehow
|
||||||
|
|
||||||
await dbi.addKey(dbKey)
|
await dbi.addKey(dbKey)
|
||||||
this.enckeys.push(dbKey)
|
this.enckeys.push(dbKey)
|
||||||
await this.startKey({ npub, sk })
|
await this.startKey({ npub, sk })
|
||||||
|
|
||||||
|
if (passphrase)
|
||||||
|
await this.saveKey(npub, passphrase)
|
||||||
|
|
||||||
// assign nip05 before adding the key
|
// assign nip05 before adding the key
|
||||||
if (!existingName && name && !name.includes('@')) {
|
if (!existingName && name && !name.includes('@')) {
|
||||||
console.log('adding key', npub, name)
|
console.log('adding key', npub, name)
|
||||||
@ -712,6 +817,10 @@ export class NoauthBackend {
|
|||||||
|
|
||||||
if (perm) {
|
if (perm) {
|
||||||
console.log('req', req, 'perm', reqPerm, 'value', perm, appPerms)
|
console.log('req', req, 'perm', reqPerm, 'value', perm, appPerms)
|
||||||
|
// connect reqs are always 'ignore' if were disallowed
|
||||||
|
if (perm.perm === 'connect' && perm.value === '0') return DECISION.IGNORE
|
||||||
|
|
||||||
|
// all other reqs are not ignored
|
||||||
return perm.value === '1' ? DECISION.ALLOW : DECISION.DISALLOW
|
return perm.value === '1' ? DECISION.ALLOW : DECISION.DISALLOW
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -724,6 +833,42 @@ export class NoauthBackend {
|
|||||||
return DECISION.ASK
|
return DECISION.ASK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async publishAppPerms({ npub, appNpub }: { npub: string; appNpub: string }) {
|
||||||
|
const key = this.keys.find((k) => k.npub === npub)
|
||||||
|
if (!key) throw new Error('Key not found')
|
||||||
|
const app = this.apps.find((a) => a.appNpub === appNpub && a.npub === npub)
|
||||||
|
if (!app) throw new Error('App not found')
|
||||||
|
const perms = this.perms.filter((p) => p.appNpub === appNpub && p.npub === npub)
|
||||||
|
const data = {
|
||||||
|
appNpub,
|
||||||
|
npub,
|
||||||
|
name: app.name,
|
||||||
|
icon: app.icon,
|
||||||
|
url: app.url,
|
||||||
|
timestamp: app.timestamp,
|
||||||
|
updateTimestamp: app.updateTimestamp,
|
||||||
|
perms,
|
||||||
|
}
|
||||||
|
const id = await this.sha256(`nsec.app_${npub}_${appNpub}`)
|
||||||
|
const { type, data: pubkey } = nip19.decode(npub)
|
||||||
|
if (type !== 'npub') throw new Error('Bad npub')
|
||||||
|
const content = await key.signer.encrypt(new NDKUser({ pubkey }), JSON.stringify(data))
|
||||||
|
const event = new NDKEvent(this.ndk, {
|
||||||
|
pubkey,
|
||||||
|
kind: KIND_DATA,
|
||||||
|
content,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [
|
||||||
|
['d', id],
|
||||||
|
['t', APP_TAG],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
event.sig = await event.sign(key.signer)
|
||||||
|
console.log('app perms event', event.rawEvent(), 'payload', data)
|
||||||
|
const relays = await event.publish(NDKRelaySet.fromRelayUrls([...OUTBOX_RELAYS, BROADCAST_RELAY], this.ndk))
|
||||||
|
console.log('app perm event published', event.id, 'to', relays)
|
||||||
|
}
|
||||||
|
|
||||||
private async connectApp({
|
private async connectApp({
|
||||||
npub,
|
npub,
|
||||||
appNpub,
|
appNpub,
|
||||||
@ -746,6 +891,7 @@ export class NoauthBackend {
|
|||||||
name: appName,
|
name: appName,
|
||||||
icon: appIcon,
|
icon: appIcon,
|
||||||
url: appUrl,
|
url: appUrl,
|
||||||
|
updateTimestamp: Date.now()
|
||||||
})
|
})
|
||||||
|
|
||||||
// reload
|
// reload
|
||||||
@ -765,6 +911,9 @@ export class NoauthBackend {
|
|||||||
|
|
||||||
// reload
|
// reload
|
||||||
this.perms = await dbi.listPerms()
|
this.perms = await dbi.listPerms()
|
||||||
|
|
||||||
|
// async
|
||||||
|
this.publishAppPerms({ npub, appNpub })
|
||||||
}
|
}
|
||||||
|
|
||||||
private async allowPermitCallback({
|
private async allowPermitCallback({
|
||||||
@ -830,6 +979,7 @@ export class NoauthBackend {
|
|||||||
name: '',
|
name: '',
|
||||||
icon: '',
|
icon: '',
|
||||||
url: options.appUrl || '',
|
url: options.appUrl || '',
|
||||||
|
updateTimestamp: Date.now()
|
||||||
})
|
})
|
||||||
|
|
||||||
// reload
|
// reload
|
||||||
@ -874,6 +1024,9 @@ export class NoauthBackend {
|
|||||||
// to this req
|
// to this req
|
||||||
ok(decision)
|
ok(decision)
|
||||||
|
|
||||||
|
// async
|
||||||
|
this.publishAppPerms({ npub: req.npub, appNpub: req.appNpub })
|
||||||
|
|
||||||
// notify UI that it was confirmed
|
// notify UI that it was confirmed
|
||||||
// if (!PERF_TEST)
|
// if (!PERF_TEST)
|
||||||
this.updateUI()
|
this.updateUI()
|
||||||
@ -933,7 +1086,7 @@ export class NoauthBackend {
|
|||||||
// looping for 10 seconds (our request age threshold)
|
// looping for 10 seconds (our request age threshold)
|
||||||
backend.rpc.sendResponse(id, remotePubkey, 'auth_url', KIND_RPC, authUrl)
|
backend.rpc.sendResponse(id, remotePubkey, 'auth_url', KIND_RPC, authUrl)
|
||||||
} else {
|
} else {
|
||||||
console.log("skip sending auth_url")
|
console.log('skip sending auth_url')
|
||||||
}
|
}
|
||||||
}, 500)
|
}, 500)
|
||||||
|
|
||||||
@ -1033,11 +1186,15 @@ export class NoauthBackend {
|
|||||||
const { data: pubkey } = nip19.decode(npub)
|
const { data: pubkey } = nip19.decode(npub)
|
||||||
const { data: appPubkey } = nip19.decode(appNpub)
|
const { data: appPubkey } = nip19.decode(appNpub)
|
||||||
|
|
||||||
const events = await this.ndk.fetchEvents({
|
const events = await this.ndk.fetchEvents(
|
||||||
kinds: [KIND_RPC],
|
{
|
||||||
'#p': [pubkey as string],
|
kinds: [KIND_RPC],
|
||||||
authors: [appPubkey as string],
|
'#p': [pubkey as string],
|
||||||
})
|
authors: [appPubkey as string],
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
NDKRelaySet.fromRelayUrls(NIP46_RELAYS, this.ndk)
|
||||||
|
)
|
||||||
console.log('fetched pending for', npub, events.size)
|
console.log('fetched pending for', npub, events.size)
|
||||||
this.pendingNpubEvents.set(npub, [...events.values()])
|
this.pendingNpubEvents.set(npub, [...events.values()])
|
||||||
}
|
}
|
||||||
@ -1057,8 +1214,8 @@ export class NoauthBackend {
|
|||||||
await this.startKey({ npub, sk })
|
await this.startKey({ npub, sk })
|
||||||
}
|
}
|
||||||
|
|
||||||
private async generateKey(name: string) {
|
private async generateKey(name: string, passphrase: string) {
|
||||||
const k = await this.addKey({ name })
|
const k = await this.addKey({ name, passphrase })
|
||||||
this.updateUI()
|
this.updateUI()
|
||||||
return k
|
return k
|
||||||
}
|
}
|
||||||
@ -1068,8 +1225,8 @@ export class NoauthBackend {
|
|||||||
await this.sendTokenToServer(npub, token)
|
await this.sendTokenToServer(npub, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async importKey(name: string, nsec: string) {
|
private async importKey(name: string, nsec: string, passphrase: string) {
|
||||||
const k = await this.addKey({ name, nsec })
|
const k = await this.addKey({ name, nsec, passphrase })
|
||||||
this.updateUI()
|
this.updateUI()
|
||||||
return k
|
return k
|
||||||
}
|
}
|
||||||
@ -1142,7 +1299,7 @@ export class NoauthBackend {
|
|||||||
enckey,
|
enckey,
|
||||||
passphrase,
|
passphrase,
|
||||||
})
|
})
|
||||||
const k = await this.addKey({ name, nsec, existingName })
|
const k = await this.addKey({ name, nsec, existingName, passphrase })
|
||||||
this.updateUI()
|
this.updateUI()
|
||||||
return k
|
return k
|
||||||
}
|
}
|
||||||
@ -1227,17 +1384,23 @@ export class NoauthBackend {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async exportKey(npub: string): Promise<string> {
|
||||||
|
const dbKey = await dbi.getKey(npub)
|
||||||
|
if (!dbKey) throw new Error("Key not found")
|
||||||
|
return dbKey.ncryptsec || ''
|
||||||
|
}
|
||||||
|
|
||||||
public async onMessage(event: any) {
|
public async onMessage(event: any) {
|
||||||
const { id, method, args } = event.data
|
const { id, method, args } = event.data
|
||||||
try {
|
try {
|
||||||
//console.log("UI message", id, method, args)
|
//console.log("UI message", id, method, args)
|
||||||
let result = undefined
|
let result = undefined
|
||||||
if (method === 'generateKey') {
|
if (method === 'generateKey') {
|
||||||
result = await this.generateKey(args[0])
|
result = await this.generateKey(args[0], args[1])
|
||||||
} else if (method === 'redeemToken') {
|
} else if (method === 'redeemToken') {
|
||||||
result = await this.redeemToken(args[0], args[1])
|
result = await this.redeemToken(args[0], args[1])
|
||||||
} else if (method === 'importKey') {
|
} else if (method === 'importKey') {
|
||||||
result = await this.importKey(args[0], args[1])
|
result = await this.importKey(args[0], args[1], args[2])
|
||||||
} else if (method === 'saveKey') {
|
} else if (method === 'saveKey') {
|
||||||
result = await this.saveKey(args[0], args[1])
|
result = await this.saveKey(args[0], args[1])
|
||||||
} else if (method === 'fetchKey') {
|
} else if (method === 'fetchKey') {
|
||||||
@ -1258,6 +1421,8 @@ export class NoauthBackend {
|
|||||||
result = await this.enablePush()
|
result = await this.enablePush()
|
||||||
} else if (method === 'fetchPendingRequests') {
|
} else if (method === 'fetchPendingRequests') {
|
||||||
result = await this.fetchPendingRequests(args[0], args[1])
|
result = await this.fetchPendingRequests(args[0], args[1])
|
||||||
|
} else if (method === 'exportKey') {
|
||||||
|
result = await this.exportKey(args[0])
|
||||||
} else {
|
} else {
|
||||||
console.log('unknown method from UI ', method)
|
console.log('unknown method from UI ', method)
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ export interface DbKey {
|
|||||||
relays?: string[]
|
relays?: string[]
|
||||||
enckey: string
|
enckey: string
|
||||||
profile?: MetaEvent | null
|
profile?: MetaEvent | null
|
||||||
|
ncryptsec?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DbApp {
|
export interface DbApp {
|
||||||
@ -18,6 +19,7 @@ export interface DbApp {
|
|||||||
icon: string
|
icon: string
|
||||||
url: string
|
url: string
|
||||||
timestamp: number
|
timestamp: number
|
||||||
|
updateTimestamp: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DbPerm {
|
export interface DbPerm {
|
||||||
@ -63,9 +65,9 @@ export interface DbSchema extends Dexie {
|
|||||||
|
|
||||||
export const db = new Dexie('noauthdb') as DbSchema
|
export const db = new Dexie('noauthdb') as DbSchema
|
||||||
|
|
||||||
db.version(8).stores({
|
db.version(9).stores({
|
||||||
keys: 'npub',
|
keys: 'npub',
|
||||||
apps: 'appNpub,npub,name,timestamp',
|
apps: 'appNpub,npub,name,timestamp,updateTimestamp',
|
||||||
perms: 'id,npub,appNpub,perm,value,timestamp',
|
perms: 'id,npub,appNpub,perm,value,timestamp',
|
||||||
pending: 'id,npub,appNpub,timestamp,method',
|
pending: 'id,npub,appNpub,timestamp,method',
|
||||||
history: 'id,npub,appNpub,timestamp,method,allowed',
|
history: 'id,npub,appNpub,timestamp,method,allowed',
|
||||||
@ -81,6 +83,13 @@ export const dbi = {
|
|||||||
console.log(`db addKey error: ${error}`)
|
console.log(`db addKey error: ${error}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getKey: async (npub: string) => {
|
||||||
|
try {
|
||||||
|
return await db.keys.get(npub)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`db getKey error: ${error}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
listKeys: async (): Promise<DbKey[]> => {
|
listKeys: async (): Promise<DbKey[]> => {
|
||||||
try {
|
try {
|
||||||
return await db.keys.toArray()
|
return await db.keys.toArray()
|
||||||
@ -113,12 +122,13 @@ export const dbi = {
|
|||||||
console.log(`db addApp error: ${error}`)
|
console.log(`db addApp error: ${error}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateApp: async (app: Omit<DbApp, 'npub' | 'timestamp'>) => {
|
updateApp: async (app: DbApp) => {
|
||||||
try {
|
try {
|
||||||
await db.apps.where({ appNpub: app.appNpub }).modify({
|
await db.apps.where({ appNpub: app.appNpub }).modify({
|
||||||
name: app.name,
|
name: app.name,
|
||||||
icon: app.icon,
|
icon: app.icon,
|
||||||
url: app.url,
|
url: app.url,
|
||||||
|
updateTimestamp: app.updateTimestamp
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`db updateApp error: ${error}`)
|
console.log(`db updateApp error: ${error}`)
|
||||||
|
@ -61,7 +61,7 @@ export class Keys {
|
|||||||
// We could use string.normalize() to make sure all JS implementations
|
// We could use string.normalize() to make sure all JS implementations
|
||||||
// are compatible, but since we're looking to make this thing a standard
|
// are compatible, but since we're looking to make this thing a standard
|
||||||
// then the simplest way is to exclude unicode and only work with ASCII
|
// then the simplest way is to exclude unicode and only work with ASCII
|
||||||
if (!isValidPassphase(passphrase)) throw new Error('Password must be 4+ ASCII chars')
|
if (!isValidPassphase(passphrase)) throw new Error('Password must be 6+ ASCII chars')
|
||||||
|
|
||||||
return new Promise((ok, fail) => {
|
return new Promise((ok, fail) => {
|
||||||
// NOTE: we should use Argon2 or scrypt later, for now
|
// NOTE: we should use Argon2 or scrypt later, for now
|
||||||
|
58
src/modules/nip49.ts
Normal file
58
src/modules/nip49.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
// copied from https://github.com/nbd-wtf/nostr-tools/blob/master/nip49.ts
|
||||||
|
// remove when we migrate to nostr-tools@2.x.x
|
||||||
|
|
||||||
|
import { scrypt } from '@noble/hashes/scrypt'
|
||||||
|
import { xchacha20poly1305 } from '@noble/ciphers/chacha'
|
||||||
|
import { concatBytes, randomBytes } from '@noble/hashes/utils'
|
||||||
|
import { bech32 } from '@scure/base'
|
||||||
|
|
||||||
|
export const Bech32MaxSize = 5000
|
||||||
|
|
||||||
|
function encodeBech32<Prefix extends string>(prefix: Prefix, data: Uint8Array): `${Prefix}1${string}` {
|
||||||
|
let words = bech32.toWords(data)
|
||||||
|
return bech32.encode(prefix, words, Bech32MaxSize) as `${Prefix}1${string}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeBytes<Prefix extends string>(prefix: Prefix, bytes: Uint8Array): `${Prefix}1${string}` {
|
||||||
|
return encodeBech32(prefix, bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encrypt(sec: Uint8Array, password: string, logn: number = 16, ksb: 0x00 | 0x01 | 0x02 = 0x02): string {
|
||||||
|
let salt = randomBytes(16)
|
||||||
|
let n = 2 ** logn
|
||||||
|
let key = scrypt(password.normalize('NFKC'), salt, { N: n, r: 8, p: 1, dkLen: 32 })
|
||||||
|
let nonce = randomBytes(24)
|
||||||
|
let aad = Uint8Array.from([ksb])
|
||||||
|
let xc2p1 = xchacha20poly1305(key, nonce, aad)
|
||||||
|
let ciphertext = xc2p1.encrypt(sec)
|
||||||
|
let b = concatBytes(Uint8Array.from([0x02]), Uint8Array.from([logn]), salt, nonce, aad, ciphertext)
|
||||||
|
return encodeBytes('ncryptsec', b)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decrypt(ncryptsec: string, password: string): Uint8Array {
|
||||||
|
let { prefix, words } = bech32.decode(ncryptsec, Bech32MaxSize)
|
||||||
|
if (prefix !== 'ncryptsec') {
|
||||||
|
throw new Error(`invalid prefix ${prefix}, expected 'ncryptsec'`)
|
||||||
|
}
|
||||||
|
let b = new Uint8Array(bech32.fromWords(words))
|
||||||
|
|
||||||
|
let version = b[0]
|
||||||
|
if (version !== 0x02) {
|
||||||
|
throw new Error(`invalid version ${version}, expected 0x02`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let logn = b[1]
|
||||||
|
let n = 2 ** logn
|
||||||
|
|
||||||
|
let salt = b.slice(2, 2 + 16)
|
||||||
|
let nonce = b.slice(2 + 16, 2 + 16 + 24)
|
||||||
|
let ksb = b[2 + 16 + 24]
|
||||||
|
let aad = Uint8Array.from([ksb])
|
||||||
|
let ciphertext = b.slice(2 + 16 + 24 + 1)
|
||||||
|
|
||||||
|
let key = scrypt(password.normalize('NFKC'), salt, { N: n, r: 8, p: 1, dkLen: 32 })
|
||||||
|
let xc2p1 = xchacha20poly1305(key, nonce, aad)
|
||||||
|
let sec = xc2p1.decrypt(ciphertext)
|
||||||
|
|
||||||
|
return sec
|
||||||
|
}
|
@ -97,6 +97,4 @@ const darkTheme: Theme = createTheme({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(darkTheme)
|
|
||||||
|
|
||||||
export { lightTheme, darkTheme }
|
export { lightTheme, darkTheme }
|
||||||
|
@ -3,11 +3,16 @@ export const WEB_PUSH_PUBKEY = process.env.REACT_APP_WEB_PUSH_PUBKEY
|
|||||||
export const DOMAIN = process.env.REACT_APP_DOMAIN
|
export const DOMAIN = process.env.REACT_APP_DOMAIN
|
||||||
export const RELAY = process.env.REACT_APP_RELAY || 'wss://relay.nsec.app'
|
export const RELAY = process.env.REACT_APP_RELAY || 'wss://relay.nsec.app'
|
||||||
export const NIP46_RELAYS = [RELAY]
|
export const NIP46_RELAYS = [RELAY]
|
||||||
|
export const OUTBOX_RELAYS = ['wss://relay.nostr.band', 'wss://nos.lol', 'wss://purplepag.es']
|
||||||
|
export const BROADCAST_RELAY = 'wss://nostr.mutinywallet.com'
|
||||||
|
|
||||||
|
export const APP_TAG = 'nsec.app/perm'
|
||||||
|
|
||||||
export const MIN_POW = 14
|
export const MIN_POW = 14
|
||||||
export const MAX_POW = 19
|
export const MAX_POW = 19
|
||||||
|
|
||||||
export const KIND_RPC = 24133
|
export const KIND_RPC = 24133
|
||||||
|
export const KIND_DATA = 30078
|
||||||
|
|
||||||
export const RELOAD_STORAGE_KEY = 'reload'
|
export const RELOAD_STORAGE_KEY = 'reload'
|
||||||
|
|
||||||
|
@ -126,7 +126,8 @@ export const getReferrerAppUrl = () => {
|
|||||||
if (!window.document.referrer) return ''
|
if (!window.document.referrer) return ''
|
||||||
try {
|
try {
|
||||||
const u = new URL(window.document.referrer.toLocaleLowerCase())
|
const u = new URL(window.document.referrer.toLocaleLowerCase())
|
||||||
if (u.hostname !== DOMAIN && !u.hostname.endsWith('.' + DOMAIN)) return u.origin
|
if (u.hostname !== DOMAIN && !u.hostname.endsWith('.' + DOMAIN) && u.origin !== window.location.origin)
|
||||||
|
return u.origin
|
||||||
} catch {}
|
} catch {}
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user