Compare commits
7 Commits
feature/pe
...
feature/ad
Author | SHA1 | Date | |
---|---|---|---|
5ca798958f | |||
33b088383d | |||
fe462376c6 | |||
a9b4b22c34 | |||
3c2d2f9f84 | |||
88f559b8f1 | |||
6a6b18bcad |
@ -147,7 +147,7 @@ export const ModalLogin = () => {
|
||||
|
||||
<Stack gap={'0.5rem'}>
|
||||
<Button type="submit" fullWidth disabled={isLoading}>
|
||||
Add account {isLoading && <LoadingSpinner />}
|
||||
Login {isLoading && <LoadingSpinner />}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
@ -2,9 +2,10 @@ import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
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 { CheckmarkIcon } from '@/assets'
|
||||
import GppMaybeOutlinedIcon from '@mui/icons-material/GppMaybeOutlined'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import { ChangeEvent, FC, useEffect, useState } from 'react'
|
||||
import { Checkbox } from '@/shared/Checkbox/Checkbox'
|
||||
@ -17,6 +18,7 @@ import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { selectKeys } from '@/store'
|
||||
import { isValidPassphase, isWeakPassphase } from '@/modules/keys'
|
||||
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
||||
import { useCopyToClipboard } from 'usehooks-ts'
|
||||
|
||||
type ModalSettingsProps = {
|
||||
isSynced: boolean
|
||||
@ -26,6 +28,7 @@ export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
const keys = useAppSelector(selectKeys)
|
||||
const [, copyToClipboard] = useCopyToClipboard()
|
||||
|
||||
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 (
|
||||
<Modal open={isModalOpened} onClose={onClose} title="Settings">
|
||||
<Stack gap={'1rem'}>
|
||||
<StyledSettingContainer onSubmit={handleSubmit}>
|
||||
<Stack direction={'row'} justifyContent={'space-between'}>
|
||||
<Stack direction={'row'} justifyContent={'space-between'} alignItems={'start'}>
|
||||
<SectionTitle>Cloud sync</SectionTitle>
|
||||
{isSynced && (
|
||||
<StyledSynchedText>
|
||||
<CheckmarkIcon /> Synched
|
||||
</StyledSynchedText>
|
||||
)}
|
||||
{!isSynced && (
|
||||
<StyledSynchText>
|
||||
{/* <GppMaybeOutlinedIcon style={{ transform: 'scale(0.8)' }} /> */}
|
||||
Not enabled
|
||||
</StyledSynchText>
|
||||
)}
|
||||
</Stack>
|
||||
<Box>
|
||||
<Checkbox onChange={handleChangeCheckbox} checked={isChecked} />
|
||||
@ -145,6 +168,18 @@ export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
||||
Sync {isLoading && <LoadingSpinner mode="secondary" />}
|
||||
</StyledButton>
|
||||
</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>
|
||||
</Modal>
|
||||
)
|
||||
|
@ -29,3 +29,11 @@ export const StyledSynchedText = styled((props: TypographyProps) => <Typography
|
||||
color: theme.palette.success.main,
|
||||
}
|
||||
})
|
||||
|
||||
export const StyledSynchText = styled((props: TypographyProps) => <Typography variant="caption" {...props} />)(({
|
||||
theme,
|
||||
}) => {
|
||||
return {
|
||||
color: theme.palette.error.main,
|
||||
}
|
||||
})
|
||||
|
@ -28,6 +28,7 @@ import {
|
||||
// import { Nip04 } from './nip04'
|
||||
import { fetchNip05, getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers'
|
||||
import { NostrPowEvent, minePow } from './pow'
|
||||
import { encrypt as encryptNip49 } from './nip49'
|
||||
//import { PrivateKeySigner } from './signer'
|
||||
|
||||
//const PERF_TEST = false
|
||||
@ -743,10 +744,12 @@ export class NoauthBackend {
|
||||
name,
|
||||
nsec,
|
||||
existingName,
|
||||
passphrase
|
||||
}: {
|
||||
name: string
|
||||
nsec?: string
|
||||
existingName?: boolean
|
||||
passphrase?: string
|
||||
}): Promise<KeyInfo> {
|
||||
// lowercase
|
||||
name = name.trim().toLocaleLowerCase()
|
||||
@ -764,12 +767,24 @@ export class NoauthBackend {
|
||||
|
||||
const localKey = await this.keysModule.generateLocalKey()
|
||||
const enckey = await this.keysModule.encryptKeyLocal(sk, localKey)
|
||||
|
||||
// @ts-ignore
|
||||
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)
|
||||
this.enckeys.push(dbKey)
|
||||
await this.startKey({ npub, sk })
|
||||
|
||||
if (passphrase)
|
||||
await this.saveKey(npub, passphrase)
|
||||
|
||||
// assign nip05 before adding the key
|
||||
if (!existingName && name && !name.includes('@')) {
|
||||
console.log('adding key', npub, name)
|
||||
@ -1199,8 +1214,8 @@ export class NoauthBackend {
|
||||
await this.startKey({ npub, sk })
|
||||
}
|
||||
|
||||
private async generateKey(name: string) {
|
||||
const k = await this.addKey({ name })
|
||||
private async generateKey(name: string, passphrase: string) {
|
||||
const k = await this.addKey({ name, passphrase })
|
||||
this.updateUI()
|
||||
return k
|
||||
}
|
||||
@ -1210,8 +1225,8 @@ export class NoauthBackend {
|
||||
await this.sendTokenToServer(npub, token)
|
||||
}
|
||||
|
||||
private async importKey(name: string, nsec: string) {
|
||||
const k = await this.addKey({ name, nsec })
|
||||
private async importKey(name: string, nsec: string, passphrase: string) {
|
||||
const k = await this.addKey({ name, nsec, passphrase })
|
||||
this.updateUI()
|
||||
return k
|
||||
}
|
||||
@ -1284,7 +1299,7 @@ export class NoauthBackend {
|
||||
enckey,
|
||||
passphrase,
|
||||
})
|
||||
const k = await this.addKey({ name, nsec, existingName })
|
||||
const k = await this.addKey({ name, nsec, existingName, passphrase })
|
||||
this.updateUI()
|
||||
return k
|
||||
}
|
||||
@ -1369,17 +1384,23 @@ export class NoauthBackend {
|
||||
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) {
|
||||
const { id, method, args } = event.data
|
||||
try {
|
||||
//console.log("UI message", id, method, args)
|
||||
let result = undefined
|
||||
if (method === 'generateKey') {
|
||||
result = await this.generateKey(args[0])
|
||||
result = await this.generateKey(args[0], args[1])
|
||||
} else if (method === 'redeemToken') {
|
||||
result = await this.redeemToken(args[0], args[1])
|
||||
} 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') {
|
||||
result = await this.saveKey(args[0], args[1])
|
||||
} else if (method === 'fetchKey') {
|
||||
@ -1400,6 +1421,8 @@ export class NoauthBackend {
|
||||
result = await this.enablePush()
|
||||
} else if (method === 'fetchPendingRequests') {
|
||||
result = await this.fetchPendingRequests(args[0], args[1])
|
||||
} else if (method === 'exportKey') {
|
||||
result = await this.exportKey(args[0])
|
||||
} else {
|
||||
console.log('unknown method from UI ', method)
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ export interface DbKey {
|
||||
relays?: string[]
|
||||
enckey: string
|
||||
profile?: MetaEvent | null
|
||||
ncryptsec?: string
|
||||
}
|
||||
|
||||
export interface DbApp {
|
||||
@ -82,6 +83,13 @@ export const dbi = {
|
||||
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[]> => {
|
||||
try {
|
||||
return await db.keys.toArray()
|
||||
|
@ -61,7 +61,7 @@ export class Keys {
|
||||
// We could use string.normalize() to make sure all JS implementations
|
||||
// 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
|
||||
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) => {
|
||||
// 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
|
||||
}
|
@ -126,7 +126,7 @@ export const getReferrerAppUrl = () => {
|
||||
if (!window.document.referrer) return ''
|
||||
try {
|
||||
const u = new URL(window.document.referrer.toLocaleLowerCase())
|
||||
if (u.hostname !== DOMAIN && !u.hostname.endsWith('.' + DOMAIN) && u.origin != window.location.origin)
|
||||
if (u.hostname !== DOMAIN && !u.hostname.endsWith('.' + DOMAIN) && u.origin !== window.location.origin)
|
||||
return u.origin
|
||||
} catch {}
|
||||
return ''
|
||||
|
Reference in New Issue
Block a user