feat: support ncryptsec
This commit is contained in:
@@ -60,7 +60,9 @@ function SignerTypeBadge({ signerType }: { signerType: TSignerType }) {
|
||||
return <Badge className=" bg-green-400 hover:bg-green-400/80">NIP-07</Badge>
|
||||
} else if (signerType === 'bunker') {
|
||||
return <Badge className=" bg-blue-400 hover:bg-blue-400/80">Bunker</Badge>
|
||||
} else if (signerType === 'ncryptsec') {
|
||||
return <Badge>NCRYPTSEC</Badge>
|
||||
} else {
|
||||
return <Badge>NSEC</Badge>
|
||||
return <Badge className=" bg-orange-400 hover:bg-orange-400/80">NSEC</Badge>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function PrivateKeyLogin({
|
||||
back,
|
||||
onLoginSuccess
|
||||
}: {
|
||||
back: () => void
|
||||
onLoginSuccess: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { nsecLogin } = useNostr()
|
||||
const [nsec, setNsec] = useState('')
|
||||
const [errMsg, setErrMsg] = useState<string | null>(null)
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNsec(e.target.value)
|
||||
setErrMsg(null)
|
||||
}
|
||||
|
||||
const handleLogin = () => {
|
||||
if (nsec === '') return
|
||||
|
||||
nsecLogin(nsec)
|
||||
.then(() => onLoginSuccess())
|
||||
.catch((err) => {
|
||||
setErrMsg(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-orange-400">
|
||||
{t(
|
||||
'Using private key login is insecure. It is recommended to use a browser extension for login, such as alby, nostr-keyx or nos2x.'
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="nsec1.."
|
||||
value={nsec}
|
||||
onChange={handleInputChange}
|
||||
className={errMsg ? 'border-destructive' : ''}
|
||||
/>
|
||||
{errMsg && <div className="text-xs text-destructive pl-3">{errMsg}</div>}
|
||||
</div>
|
||||
<Button onClick={handleLogin}>{t('Login')}</Button>
|
||||
<Button variant="secondary" onClick={back}>
|
||||
{t('Back')}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
142
src/components/AccountManager/PrivateKeyLogin.tsx
Normal file
142
src/components/AccountManager/PrivateKeyLogin.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function PrivateKeyLogin({
|
||||
back,
|
||||
onLoginSuccess
|
||||
}: {
|
||||
back: () => void
|
||||
onLoginSuccess: () => void
|
||||
}) {
|
||||
return (
|
||||
<Tabs defaultValue="nsec">
|
||||
<TabsList>
|
||||
<TabsTrigger value="nsec">nsec</TabsTrigger>
|
||||
<TabsTrigger value="ncryptsec">ncryptsec</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="nsec">
|
||||
<NsecLogin back={back} onLoginSuccess={onLoginSuccess} />
|
||||
</TabsContent>
|
||||
<TabsContent value="ncryptsec">
|
||||
<NcryptsecLogin back={back} onLoginSuccess={onLoginSuccess} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
function NsecLogin({ back, onLoginSuccess }: { back: () => void; onLoginSuccess: () => void }) {
|
||||
const { t } = useTranslation()
|
||||
const { nsecLogin } = useNostr()
|
||||
const [nsec, setNsec] = useState('')
|
||||
const [errMsg, setErrMsg] = useState<string | null>(null)
|
||||
const [password, setPassword] = useState('')
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNsec(e.target.value)
|
||||
setErrMsg(null)
|
||||
}
|
||||
|
||||
const handleLogin = () => {
|
||||
if (nsec === '') return
|
||||
|
||||
nsecLogin(nsec, password)
|
||||
.then(() => onLoginSuccess())
|
||||
.catch((err) => {
|
||||
setErrMsg(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="text-orange-400">
|
||||
{t(
|
||||
'Using private key login is insecure. It is recommended to use a browser extension for login, such as alby, nostr-keyx or nos2x.'
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-muted-foreground text-sm font-semibold">nsec</div>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="nsec1.."
|
||||
value={nsec}
|
||||
onChange={handleInputChange}
|
||||
className={errMsg ? 'border-destructive' : ''}
|
||||
/>
|
||||
{errMsg && <div className="text-xs text-destructive pl-3">{errMsg}</div>}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-muted-foreground text-sm font-semibold">{t('password')}</div>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={t('optional: encrypt nsec')}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button className="w-full" onClick={handleLogin}>
|
||||
{t('Login')}
|
||||
</Button>
|
||||
<Button className="w-full" variant="secondary" onClick={back}>
|
||||
{t('Back')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NcryptsecLogin({
|
||||
back,
|
||||
onLoginSuccess
|
||||
}: {
|
||||
back: () => void
|
||||
onLoginSuccess: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { ncryptsecLogin } = useNostr()
|
||||
const [ncryptsec, setNcryptsec] = useState('')
|
||||
const [errMsg, setErrMsg] = useState<string | null>(null)
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNcryptsec(e.target.value)
|
||||
setErrMsg(null)
|
||||
}
|
||||
|
||||
const handleLogin = () => {
|
||||
if (ncryptsec === '') return
|
||||
|
||||
ncryptsecLogin(ncryptsec)
|
||||
.then(() => onLoginSuccess())
|
||||
.catch((err) => {
|
||||
setErrMsg(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="text-orange-400">
|
||||
{t(
|
||||
'Using private key login is insecure. It is recommended to use a browser extension for login, such as alby, nostr-keyx or nos2x.'
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="ncryptsec1.."
|
||||
value={ncryptsec}
|
||||
onChange={handleInputChange}
|
||||
className={errMsg ? 'border-destructive' : ''}
|
||||
/>
|
||||
{errMsg && <div className="text-xs text-destructive pl-3">{errMsg}</div>}
|
||||
</div>
|
||||
<Button className="w-full" onClick={handleLogin}>
|
||||
{t('Login')}
|
||||
</Button>
|
||||
<Button className="w-full" variant="secondary" onClick={back}>
|
||||
{t('Back')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AccountList from '../AccountList'
|
||||
import BunkerLogin from './BunkerLogin'
|
||||
import PrivateKeyLogin from './NsecLogin'
|
||||
import PrivateKeyLogin from './PrivateKeyLogin'
|
||||
import GenerateNewAccount from './GenerateNewAccount'
|
||||
|
||||
type TAccountManagerPage = 'nsec' | 'bunker' | 'generate' | null
|
||||
|
||||
@@ -138,6 +138,10 @@ export default {
|
||||
Bio: 'Bio',
|
||||
'Nostr Address (NIP-05)': 'Nostr Address (NIP-05)',
|
||||
'Invalid NIP-05 address': 'Invalid NIP-05 address',
|
||||
'Copy private key (nsec)': 'Copy private key (nsec)'
|
||||
'Copy private key': 'Copy private key',
|
||||
'Enter the password to decrypt your ncryptsec': 'Enter the password to decrypt your ncryptsec',
|
||||
Back: 'Back',
|
||||
'optional: encrypt nsec': 'optional: encrypt nsec',
|
||||
password: 'password'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +138,11 @@ export default {
|
||||
Bio: '简介',
|
||||
'Nostr Address (NIP-05)': 'Nostr 地址 (NIP-05)',
|
||||
'Invalid NIP-05 address': '无效的 NIP-05 地址',
|
||||
'Copy private key (nsec)': '复制私钥 (nsec)'
|
||||
'Copy private key': '复制私钥',
|
||||
'Enter the password to decrypt your ncryptsec': '输入密码以解密您的 ncryptsec',
|
||||
Back: '返回',
|
||||
'password (optional): encrypt nsec': '密码 (可选): 加密 nsec',
|
||||
'optional: encrypt nsec': '可选: 加密 nsec',
|
||||
password: '密码'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,12 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function SettingsPage({ index }: { index?: number }) {
|
||||
const { t, i18n } = useTranslation()
|
||||
const { nsec } = useNostr()
|
||||
const { nsec, ncryptsec } = useNostr()
|
||||
const { push } = useSecondaryPage()
|
||||
const [language, setLanguage] = useState<TLanguage>(i18n.language as TLanguage)
|
||||
const { themeSetting, setThemeSetting } = useTheme()
|
||||
const [copiedNsec, setCopiedNsec] = useState(false)
|
||||
const [copiedNcryptsec, setCopiedNcryptsec] = useState(false)
|
||||
|
||||
const handleLanguageChange = (value: TLanguage) => {
|
||||
i18n.changeLanguage(value)
|
||||
@@ -75,11 +76,26 @@ export default function SettingsPage({ index }: { index?: number }) {
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<KeyRound />
|
||||
<div>{t('Copy private key (nsec)')}</div>
|
||||
<div>{t('Copy private key')} (nsec)</div>
|
||||
</div>
|
||||
{copiedNsec ? <Check /> : <Copy />}
|
||||
</SettingItem>
|
||||
)}
|
||||
{!!ncryptsec && (
|
||||
<SettingItem
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(ncryptsec)
|
||||
setCopiedNcryptsec(true)
|
||||
setTimeout(() => setCopiedNcryptsec(false), 2000)
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<KeyRound />
|
||||
<div>{t('Copy private key')} (ncryptsec)</div>
|
||||
</div>
|
||||
{copiedNcryptsec ? <Check /> : <Copy />}
|
||||
</SettingItem>
|
||||
)}
|
||||
<AboutInfoDialog>
|
||||
<SettingItem>
|
||||
<div className="flex items-center gap-4">
|
||||
|
||||
@@ -12,10 +12,13 @@ import storage from '@/services/storage.service'
|
||||
import { ISigner, TAccount, TAccountPointer, TDraftEvent, TProfile, TRelayList } from '@/types'
|
||||
import dayjs from 'dayjs'
|
||||
import { Event, kinds } from 'nostr-tools'
|
||||
import * as nip19 from 'nostr-tools/nip19'
|
||||
import * as nip49 from 'nostr-tools/nip49'
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import { BunkerSigner } from './bunker.signer'
|
||||
import { Nip07Signer } from './nip-07.signer'
|
||||
import { NsecSigner } from './nsec.signer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type TNostrContext = {
|
||||
pubkey: string | null
|
||||
@@ -26,8 +29,10 @@ type TNostrContext = {
|
||||
account: TAccountPointer | null
|
||||
accounts: TAccountPointer[]
|
||||
nsec: string | null
|
||||
ncryptsec: string | null
|
||||
switchAccount: (account: TAccountPointer | null) => Promise<void>
|
||||
nsecLogin: (nsec: string) => Promise<string>
|
||||
nsecLogin: (nsec: string, password?: string) => Promise<string>
|
||||
ncryptsecLogin: (ncryptsec: string) => Promise<string>
|
||||
nip07Login: () => Promise<string>
|
||||
bunkerLogin: (bunker: string) => Promise<string>
|
||||
removeAccount: (account: TAccountPointer) => void
|
||||
@@ -56,9 +61,11 @@ export const useNostr = () => {
|
||||
}
|
||||
|
||||
export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||
const { t } = useTranslation()
|
||||
const { toast } = useToast()
|
||||
const [account, setAccount] = useState<TAccountPointer | null>(null)
|
||||
const [nsec, setNsec] = useState<string | null>(null)
|
||||
const [ncryptsec, setNcryptsec] = useState<string | null>(null)
|
||||
const [signer, setSigner] = useState<ISigner | null>(null)
|
||||
const [openLoginDialog, setOpenLoginDialog] = useState(false)
|
||||
const [profile, setProfile] = useState<TProfile | null>(null)
|
||||
@@ -90,6 +97,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||
const storedNsec = storage.getAccountNsec(account.pubkey)
|
||||
if (storedNsec) {
|
||||
setNsec(storedNsec)
|
||||
} else {
|
||||
setNsec(null)
|
||||
}
|
||||
const storedNcryptsec = storage.getAccountNcryptsec(account.pubkey)
|
||||
if (storedNcryptsec) {
|
||||
setNcryptsec(storedNcryptsec)
|
||||
} else {
|
||||
setNcryptsec(null)
|
||||
}
|
||||
const storedRelayListEvent = storage.getAccountRelayListEvent(account.pubkey)
|
||||
if (storedRelayListEvent) {
|
||||
@@ -171,12 +186,31 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||
await loginWithAccountPointer(act)
|
||||
}
|
||||
|
||||
const nsecLogin = async (nsec: string) => {
|
||||
const nsecLogin = async (nsec: string, password?: string) => {
|
||||
const browserNsecSigner = new NsecSigner()
|
||||
const pubkey = browserNsecSigner.login(nsec)
|
||||
const { type, data: privkey } = nip19.decode(nsec)
|
||||
if (type !== 'nsec') {
|
||||
throw new Error('invalid nsec')
|
||||
}
|
||||
const pubkey = browserNsecSigner.login(privkey)
|
||||
if (password) {
|
||||
const ncryptsec = nip49.encrypt(privkey, password)
|
||||
return login(browserNsecSigner, { pubkey, signerType: 'ncryptsec', ncryptsec })
|
||||
}
|
||||
return login(browserNsecSigner, { pubkey, signerType: 'nsec', nsec })
|
||||
}
|
||||
|
||||
const ncryptsecLogin = async (ncryptsec: string) => {
|
||||
const password = prompt(t('Enter the password to decrypt your ncryptsec'))
|
||||
if (!password) {
|
||||
throw new Error('Password is required')
|
||||
}
|
||||
const privkey = nip49.decrypt(ncryptsec, password)
|
||||
const browserNsecSigner = new NsecSigner()
|
||||
const pubkey = browserNsecSigner.login(privkey)
|
||||
return login(browserNsecSigner, { pubkey, signerType: 'ncryptsec', ncryptsec })
|
||||
}
|
||||
|
||||
const nip07Login = async () => {
|
||||
try {
|
||||
const nip07Signer = new Nip07Signer()
|
||||
@@ -228,6 +262,17 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
return login(browserNsecSigner, account)
|
||||
}
|
||||
} else if (account.signerType === 'ncryptsec') {
|
||||
if (account.ncryptsec) {
|
||||
const password = prompt(t('Enter the password to decrypt your ncryptsec'))
|
||||
if (!password) {
|
||||
return null
|
||||
}
|
||||
const privkey = nip49.decrypt(account.ncryptsec, password)
|
||||
const browserNsecSigner = new NsecSigner()
|
||||
browserNsecSigner.login(privkey)
|
||||
return login(browserNsecSigner, account)
|
||||
}
|
||||
} else if (account.signerType === 'nip-07') {
|
||||
const nip07Signer = new Nip07Signer()
|
||||
return login(nip07Signer, account)
|
||||
@@ -334,8 +379,10 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||
.getAccounts()
|
||||
.map((act) => ({ pubkey: act.pubkey, signerType: act.signerType })),
|
||||
nsec,
|
||||
ncryptsec,
|
||||
switchAccount,
|
||||
nsecLogin,
|
||||
ncryptsecLogin,
|
||||
nip07Login,
|
||||
bunkerLogin,
|
||||
removeAccount,
|
||||
|
||||
@@ -5,14 +5,20 @@ export class NsecSigner implements ISigner {
|
||||
private privkey: Uint8Array | null = null
|
||||
private pubkey: string | null = null
|
||||
|
||||
login(nsec: string) {
|
||||
const { type, data } = nip19.decode(nsec)
|
||||
if (type !== 'nsec') {
|
||||
throw new Error('invalid nsec')
|
||||
login(nsecOrPrivkey: string | Uint8Array) {
|
||||
let privkey
|
||||
if (typeof nsecOrPrivkey === 'string') {
|
||||
const { type, data } = nip19.decode(nsecOrPrivkey)
|
||||
if (type !== 'nsec') {
|
||||
throw new Error('invalid nsec')
|
||||
}
|
||||
privkey = data
|
||||
} else {
|
||||
privkey = nsecOrPrivkey
|
||||
}
|
||||
|
||||
this.privkey = data
|
||||
this.pubkey = nGetPublicKey(data)
|
||||
this.privkey = privkey
|
||||
this.pubkey = nGetPublicKey(privkey)
|
||||
return this.pubkey
|
||||
}
|
||||
|
||||
|
||||
@@ -156,6 +156,13 @@ class StorageService {
|
||||
return account?.nsec
|
||||
}
|
||||
|
||||
getAccountNcryptsec(pubkey: string) {
|
||||
const account = this.accounts.find(
|
||||
(act) => act.pubkey === pubkey && act.signerType === 'ncryptsec'
|
||||
)
|
||||
return account?.ncryptsec
|
||||
}
|
||||
|
||||
addAccount(account: TAccount) {
|
||||
if (this.accounts.find((act) => isSameAccount(act, account))) {
|
||||
return
|
||||
|
||||
@@ -53,11 +53,12 @@ export interface ISigner {
|
||||
signEvent: (draftEvent: TDraftEvent) => Promise<Event | null>
|
||||
}
|
||||
|
||||
export type TSignerType = 'nsec' | 'nip-07' | 'bunker' | 'browser-nsec'
|
||||
export type TSignerType = 'nsec' | 'nip-07' | 'bunker' | 'browser-nsec' | 'ncryptsec'
|
||||
|
||||
export type TAccount = {
|
||||
pubkey: string
|
||||
signerType: TSignerType
|
||||
ncryptsec?: string
|
||||
nsec?: string
|
||||
bunker?: string
|
||||
bunkerClientSecretKey?: string
|
||||
|
||||
Reference in New Issue
Block a user