Fix unintended sharing of wallets and logs (#1127)
* Suffix localStorage key for attached wallets with me.id * Suffix IndexedDB database name with me.id * Fix TypeError: Cannot destructure property of 'config' as it is null * Detach wallet on logout * Migrate to new storage keys * Use Promise.catch for togglePushSubscription on logout It's more concise --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
This commit is contained in:
parent
72c27e339c
commit
5c593ce280
@ -126,14 +126,14 @@ export function useServiceWorkerLogger () {
|
|||||||
const WalletLoggerContext = createContext()
|
const WalletLoggerContext = createContext()
|
||||||
const WalletLogsContext = createContext()
|
const WalletLogsContext = createContext()
|
||||||
|
|
||||||
const initIndexedDB = async (storeName) => {
|
const initIndexedDB = async (dbName, storeName) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!window.indexedDB) {
|
if (!window.indexedDB) {
|
||||||
return reject(new Error('IndexedDB not supported'))
|
return reject(new Error('IndexedDB not supported'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
|
// https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
|
||||||
const request = window.indexedDB.open('app:storage', 1)
|
const request = window.indexedDB.open(dbName, 1)
|
||||||
|
|
||||||
let db
|
let db
|
||||||
request.onupgradeneeded = () => {
|
request.onupgradeneeded = () => {
|
||||||
@ -159,7 +159,12 @@ const initIndexedDB = async (storeName) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const WalletLoggerProvider = ({ children }) => {
|
const WalletLoggerProvider = ({ children }) => {
|
||||||
|
const me = useMe()
|
||||||
const [logs, setLogs] = useState([])
|
const [logs, setLogs] = useState([])
|
||||||
|
let dbName = 'app:storage'
|
||||||
|
if (me) {
|
||||||
|
dbName = `${dbName}:${me.id}`
|
||||||
|
}
|
||||||
const idbStoreName = 'wallet_logs'
|
const idbStoreName = 'wallet_logs'
|
||||||
const idb = useRef()
|
const idb = useRef()
|
||||||
const logQueue = useRef([])
|
const logQueue = useRef([])
|
||||||
@ -211,7 +216,7 @@ const WalletLoggerProvider = ({ children }) => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initIndexedDB(idbStoreName)
|
initIndexedDB(dbName, idbStoreName)
|
||||||
.then(db => {
|
.then(db => {
|
||||||
idb.current = db
|
idb.current = db
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ import classNames from 'classnames'
|
|||||||
import SnIcon from '@/svgs/sn.svg'
|
import SnIcon from '@/svgs/sn.svg'
|
||||||
import { useHasNewNotes } from '../use-has-new-notes'
|
import { useHasNewNotes } from '../use-has-new-notes'
|
||||||
import { useWalletLogger } from '../logger'
|
import { useWalletLogger } from '../logger'
|
||||||
|
import { useWebLNConfigurator } from '../webln'
|
||||||
|
|
||||||
export function Brand ({ className }) {
|
export function Brand ({ className }) {
|
||||||
return (
|
return (
|
||||||
@ -162,8 +163,6 @@ export function NavWalletSummary ({ className }) {
|
|||||||
|
|
||||||
export function MeDropdown ({ me, dropNavKey }) {
|
export function MeDropdown ({ me, dropNavKey }) {
|
||||||
if (!me) return null
|
if (!me) return null
|
||||||
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
|
||||||
const { deleteLogs } = useWalletLogger()
|
|
||||||
return (
|
return (
|
||||||
<div className='position-relative'>
|
<div className='position-relative'>
|
||||||
<Dropdown className={styles.dropdown} align='end'>
|
<Dropdown className={styles.dropdown} align='end'>
|
||||||
@ -202,22 +201,7 @@ export function MeDropdown ({ me, dropNavKey }) {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Dropdown.Divider />
|
<Dropdown.Divider />
|
||||||
<Dropdown.Item
|
<LogoutDropdownItem />
|
||||||
onClick={async () => {
|
|
||||||
// order is important because we need to be logged in to delete push subscription on server
|
|
||||||
const pushSubscription = await swRegistration?.pushManager.getSubscription()
|
|
||||||
if (pushSubscription) {
|
|
||||||
// don't prevent signout because of an unsubscription error
|
|
||||||
await togglePushSubscription().catch(console.error)
|
|
||||||
}
|
|
||||||
// delete client wallet logs to prevent leak of private data if a shared device was used
|
|
||||||
await deleteLogs(Wallet.NWC).catch(console.error)
|
|
||||||
await deleteLogs(Wallet.LNbits).catch(console.error)
|
|
||||||
await deleteLogs(Wallet.LNC).catch(console.error)
|
|
||||||
await signOut({ callbackUrl: '/' })
|
|
||||||
}}
|
|
||||||
>logout
|
|
||||||
</Dropdown.Item>
|
|
||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
{!me.bioId &&
|
{!me.bioId &&
|
||||||
@ -271,6 +255,31 @@ export default function LoginButton ({ className }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function LogoutDropdownItem () {
|
||||||
|
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
||||||
|
const webLN = useWebLNConfigurator()
|
||||||
|
const { deleteLogs } = useWalletLogger()
|
||||||
|
return (
|
||||||
|
<Dropdown.Item
|
||||||
|
onClick={async () => {
|
||||||
|
// order is important because we need to be logged in to delete push subscription on server
|
||||||
|
const pushSubscription = await swRegistration?.pushManager.getSubscription()
|
||||||
|
if (pushSubscription) {
|
||||||
|
await togglePushSubscription().catch(console.error)
|
||||||
|
}
|
||||||
|
// detach wallets
|
||||||
|
await webLN.clearConfig().catch(console.error)
|
||||||
|
// delete client wallet logs to prevent leak of private data if a shared device was used
|
||||||
|
await deleteLogs(Wallet.NWC).catch(console.error)
|
||||||
|
await deleteLogs(Wallet.LNbits).catch(console.error)
|
||||||
|
await deleteLogs(Wallet.LNC).catch(console.error)
|
||||||
|
await signOut({ callbackUrl: '/' })
|
||||||
|
}}
|
||||||
|
>logout
|
||||||
|
</Dropdown.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function LoginButtons () {
|
export function LoginButtons () {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -2,9 +2,7 @@ import { useState } from 'react'
|
|||||||
import { Dropdown, Image, Nav, Navbar, Offcanvas } from 'react-bootstrap'
|
import { Dropdown, Image, Nav, Navbar, Offcanvas } from 'react-bootstrap'
|
||||||
import { MEDIA_URL } from '@/lib/constants'
|
import { MEDIA_URL } from '@/lib/constants'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useServiceWorker } from '@/components/serviceworker'
|
import { LoginButtons, LogoutDropdownItem, NavWalletSummary } from '../common'
|
||||||
import { signOut } from 'next-auth/react'
|
|
||||||
import { LoginButtons, NavWalletSummary } from '../common'
|
|
||||||
import AnonIcon from '@/svgs/spy-fill.svg'
|
import AnonIcon from '@/svgs/spy-fill.svg'
|
||||||
import styles from './footer.module.css'
|
import styles from './footer.module.css'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
@ -14,7 +12,6 @@ export default function OffCanvas ({ me, dropNavKey }) {
|
|||||||
|
|
||||||
const handleClose = () => setShow(false)
|
const handleClose = () => setShow(false)
|
||||||
const handleShow = () => setShow(true)
|
const handleShow = () => setShow(true)
|
||||||
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
|
||||||
|
|
||||||
const MeImage = ({ onClick }) => me
|
const MeImage = ({ onClick }) => me
|
||||||
? (
|
? (
|
||||||
@ -78,22 +75,7 @@ export default function OffCanvas ({ me, dropNavKey }) {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Dropdown.Divider />
|
<Dropdown.Divider />
|
||||||
<Dropdown.Item
|
<LogoutDropdownItem />
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
// order is important because we need to be logged in to delete push subscription on server
|
|
||||||
const pushSubscription = await swRegistration?.pushManager.getSubscription()
|
|
||||||
if (pushSubscription) {
|
|
||||||
await togglePushSubscription()
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// don't prevent signout because of an unsubscription error
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
await signOut({ callbackUrl: '/' })
|
|
||||||
}}
|
|
||||||
>logout
|
|
||||||
</Dropdown.Item>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
: <LoginButtons />}
|
: <LoginButtons />}
|
||||||
|
@ -33,6 +33,15 @@ export const Status = {
|
|||||||
Error: 'Error'
|
Error: 'Error'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function migrateLocalStorage (oldStorageKey, newStorageKey) {
|
||||||
|
const item = window.localStorage.getItem(oldStorageKey)
|
||||||
|
if (item) {
|
||||||
|
window.localStorage.setItem(newStorageKey, item)
|
||||||
|
window.localStorage.removeItem(oldStorageKey)
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
function RawWebLNProvider ({ children }) {
|
function RawWebLNProvider ({ children }) {
|
||||||
const lnbits = useLNbits()
|
const lnbits = useLNbits()
|
||||||
const nwc = useNWC()
|
const nwc = useNWC()
|
||||||
@ -114,8 +123,14 @@ function RawWebLNProvider ({ children }) {
|
|||||||
})
|
})
|
||||||
}, [setEnabledProviders])
|
}, [setEnabledProviders])
|
||||||
|
|
||||||
|
const clearConfig = useCallback(async () => {
|
||||||
|
lnbits.clearConfig()
|
||||||
|
nwc.clearConfig()
|
||||||
|
await lnc.clearConfig()
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WebLNContext.Provider value={{ provider: isEnabled(provider) ? { sendPayment: sendPaymentWithToast } : null, enabledProviders, setProvider }}>
|
<WebLNContext.Provider value={{ provider: isEnabled(provider) ? { sendPayment: sendPaymentWithToast } : null, enabledProviders, setProvider, clearConfig }}>
|
||||||
{children}
|
{children}
|
||||||
</WebLNContext.Provider>
|
</WebLNContext.Provider>
|
||||||
)
|
)
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||||
import { useWalletLogger } from '../logger'
|
import { useWalletLogger } from '../logger'
|
||||||
import { Status } from '.'
|
import { Status, migrateLocalStorage } from '.'
|
||||||
import { bolt11Tags } from '@/lib/bolt11'
|
import { bolt11Tags } from '@/lib/bolt11'
|
||||||
import { Wallet } from '@/lib/constants'
|
import { Wallet } from '@/lib/constants'
|
||||||
|
import { useMe } from '../me'
|
||||||
|
|
||||||
// Reference: https://github.com/getAlby/bitcoin-connect/blob/v3.2.0-alpha/src/connectors/LnbitsConnector.ts
|
// Reference: https://github.com/getAlby/bitcoin-connect/blob/v3.2.0-alpha/src/connectors/LnbitsConnector.ts
|
||||||
|
|
||||||
@ -65,13 +66,17 @@ const getPayment = async (baseUrl, adminKey, paymentHash) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LNbitsProvider ({ children }) {
|
export function LNbitsProvider ({ children }) {
|
||||||
|
const me = useMe()
|
||||||
const [url, setUrl] = useState('')
|
const [url, setUrl] = useState('')
|
||||||
const [adminKey, setAdminKey] = useState('')
|
const [adminKey, setAdminKey] = useState('')
|
||||||
const [status, setStatus] = useState()
|
const [status, setStatus] = useState()
|
||||||
const { logger } = useWalletLogger(Wallet.LNbits)
|
const { logger } = useWalletLogger(Wallet.LNbits)
|
||||||
|
|
||||||
const name = 'LNbits'
|
const name = 'LNbits'
|
||||||
const storageKey = 'webln:provider:lnbits'
|
let storageKey = 'webln:provider:lnbits'
|
||||||
|
if (me) {
|
||||||
|
storageKey = `${storageKey}:${me.id}`
|
||||||
|
}
|
||||||
|
|
||||||
const getInfo = useCallback(async () => {
|
const getInfo = useCallback(async () => {
|
||||||
const response = await getWallet(url, adminKey)
|
const response = await getWallet(url, adminKey)
|
||||||
@ -110,12 +115,19 @@ export function LNbitsProvider ({ children }) {
|
|||||||
}, [logger, url, adminKey])
|
}, [logger, url, adminKey])
|
||||||
|
|
||||||
const loadConfig = useCallback(async () => {
|
const loadConfig = useCallback(async () => {
|
||||||
const configStr = window.localStorage.getItem(storageKey)
|
let configStr = window.localStorage.getItem(storageKey)
|
||||||
setStatus(Status.Initialized)
|
setStatus(Status.Initialized)
|
||||||
|
if (!configStr) {
|
||||||
|
if (me) {
|
||||||
|
// backwards compatibility: try old storageKey
|
||||||
|
const oldStorageKey = storageKey.split(':').slice(0, -1).join(':')
|
||||||
|
configStr = migrateLocalStorage(oldStorageKey, storageKey)
|
||||||
|
}
|
||||||
if (!configStr) {
|
if (!configStr) {
|
||||||
logger.info('no existing config found')
|
logger.info('no existing config found')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const config = JSON.parse(configStr)
|
const config = JSON.parse(configStr)
|
||||||
|
|
||||||
@ -141,7 +153,7 @@ export function LNbitsProvider ({ children }) {
|
|||||||
logger.info('wallet disabled')
|
logger.info('wallet disabled')
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}, [logger])
|
}, [me, logger])
|
||||||
|
|
||||||
const saveConfig = useCallback(async (config) => {
|
const saveConfig = useCallback(async (config) => {
|
||||||
// immediately store config so it's not lost even if config is invalid
|
// immediately store config so it's not lost even if config is invalid
|
||||||
|
@ -1,20 +1,23 @@
|
|||||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||||
import { useWalletLogger } from '../logger'
|
import { useWalletLogger } from '../logger'
|
||||||
import LNC from '@lightninglabs/lnc-web'
|
import LNC from '@lightninglabs/lnc-web'
|
||||||
import { Status } from '.'
|
import { Status, migrateLocalStorage } from '.'
|
||||||
import { bolt11Tags } from '@/lib/bolt11'
|
import { bolt11Tags } from '@/lib/bolt11'
|
||||||
import useModal from '../modal'
|
import useModal from '../modal'
|
||||||
import { Form, PasswordInput, SubmitButton } from '../form'
|
import { Form, PasswordInput, SubmitButton } from '../form'
|
||||||
import CancelButton from '../cancel-button'
|
import CancelButton from '../cancel-button'
|
||||||
import { Mutex } from 'async-mutex'
|
import { Mutex } from 'async-mutex'
|
||||||
import { Wallet } from '@/lib/constants'
|
import { Wallet } from '@/lib/constants'
|
||||||
|
import { useMe } from '../me'
|
||||||
|
|
||||||
const LNCContext = createContext()
|
const LNCContext = createContext()
|
||||||
const mutex = new Mutex()
|
const mutex = new Mutex()
|
||||||
|
|
||||||
async function getLNC () {
|
async function getLNC ({ me }) {
|
||||||
if (window.lnc) return window.lnc
|
if (window.lnc) return window.lnc
|
||||||
window.lnc = new LNC({ })
|
// backwards compatibility: migrate to new storage key
|
||||||
|
if (me) migrateLocalStorage('lnc-web:default', `lnc-web:${me.id}`)
|
||||||
|
window.lnc = new LNC({ namespace: me?.id })
|
||||||
return window.lnc
|
return window.lnc
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,6 +36,7 @@ function validateNarrowPerms (lnc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LNCProvider ({ children }) {
|
export function LNCProvider ({ children }) {
|
||||||
|
const me = useMe()
|
||||||
const { logger } = useWalletLogger(Wallet.LNC)
|
const { logger } = useWalletLogger(Wallet.LNC)
|
||||||
const [config, setConfig] = useState({})
|
const [config, setConfig] = useState({})
|
||||||
const [lnc, setLNC] = useState()
|
const [lnc, setLNC] = useState()
|
||||||
@ -165,7 +169,7 @@ export function LNCProvider ({ children }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const lnc = await getLNC()
|
const lnc = await getLNC({ me })
|
||||||
setLNC(lnc)
|
setLNC(lnc)
|
||||||
setStatus(Status.Initialized)
|
setStatus(Status.Initialized)
|
||||||
if (lnc.credentials.isPaired) {
|
if (lnc.credentials.isPaired) {
|
||||||
@ -185,7 +189,7 @@ export function LNCProvider ({ children }) {
|
|||||||
setStatus(Status.Error)
|
setStatus(Status.Error)
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
}, [setStatus, setConfig, logger])
|
}, [me, setStatus, setConfig, logger])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LNCContext.Provider value={{ name: 'lnc', status, unlock, getInfo, sendPayment, config, saveConfig, clearConfig }}>
|
<LNCContext.Provider value={{ name: 'lnc', status, unlock, getInfo, sendPayment, config, saveConfig, clearConfig }}>
|
||||||
|
@ -4,13 +4,15 @@ import { createContext, useCallback, useContext, useEffect, useState } from 'rea
|
|||||||
import { Relay, finalizeEvent, nip04 } from 'nostr-tools'
|
import { Relay, finalizeEvent, nip04 } from 'nostr-tools'
|
||||||
import { parseNwcUrl } from '@/lib/url'
|
import { parseNwcUrl } from '@/lib/url'
|
||||||
import { useWalletLogger } from '../logger'
|
import { useWalletLogger } from '../logger'
|
||||||
import { Status } from '.'
|
import { Status, migrateLocalStorage } from '.'
|
||||||
import { bolt11Tags } from '@/lib/bolt11'
|
import { bolt11Tags } from '@/lib/bolt11'
|
||||||
import { Wallet } from '@/lib/constants'
|
import { Wallet } from '@/lib/constants'
|
||||||
|
import { useMe } from '../me'
|
||||||
|
|
||||||
const NWCContext = createContext()
|
const NWCContext = createContext()
|
||||||
|
|
||||||
export function NWCProvider ({ children }) {
|
export function NWCProvider ({ children }) {
|
||||||
|
const me = useMe()
|
||||||
const [nwcUrl, setNwcUrl] = useState('')
|
const [nwcUrl, setNwcUrl] = useState('')
|
||||||
const [walletPubkey, setWalletPubkey] = useState()
|
const [walletPubkey, setWalletPubkey] = useState()
|
||||||
const [relayUrl, setRelayUrl] = useState()
|
const [relayUrl, setRelayUrl] = useState()
|
||||||
@ -19,7 +21,10 @@ export function NWCProvider ({ children }) {
|
|||||||
const { logger } = useWalletLogger(Wallet.NWC)
|
const { logger } = useWalletLogger(Wallet.NWC)
|
||||||
|
|
||||||
const name = 'NWC'
|
const name = 'NWC'
|
||||||
const storageKey = 'webln:provider:nwc'
|
let storageKey = 'webln:provider:nwc'
|
||||||
|
if (me) {
|
||||||
|
storageKey = `${storageKey}:${me.id}`
|
||||||
|
}
|
||||||
|
|
||||||
const getInfo = useCallback(async (relayUrl, walletPubkey) => {
|
const getInfo = useCallback(async (relayUrl, walletPubkey) => {
|
||||||
logger.info(`requesting info event from ${relayUrl}`)
|
logger.info(`requesting info event from ${relayUrl}`)
|
||||||
@ -97,12 +102,19 @@ export function NWCProvider ({ children }) {
|
|||||||
}, [logger])
|
}, [logger])
|
||||||
|
|
||||||
const loadConfig = useCallback(async () => {
|
const loadConfig = useCallback(async () => {
|
||||||
const configStr = window.localStorage.getItem(storageKey)
|
let configStr = window.localStorage.getItem(storageKey)
|
||||||
setStatus(Status.Initialized)
|
setStatus(Status.Initialized)
|
||||||
|
if (!configStr) {
|
||||||
|
if (me) {
|
||||||
|
// backwards compatibility: try old storageKey
|
||||||
|
const oldStorageKey = storageKey.split(':').slice(0, -1).join(':')
|
||||||
|
configStr = migrateLocalStorage(oldStorageKey, storageKey)
|
||||||
|
}
|
||||||
if (!configStr) {
|
if (!configStr) {
|
||||||
logger.info('no existing config found')
|
logger.info('no existing config found')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const config = JSON.parse(configStr)
|
const config = JSON.parse(configStr)
|
||||||
|
|
||||||
@ -130,7 +142,7 @@ export function NWCProvider ({ children }) {
|
|||||||
logger.info('wallet disabled')
|
logger.info('wallet disabled')
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}, [validateParams, logger])
|
}, [me, validateParams, logger])
|
||||||
|
|
||||||
const saveConfig = useCallback(async (config) => {
|
const saveConfig = useCallback(async (config) => {
|
||||||
// immediately store config so it's not lost even if config is invalid
|
// immediately store config so it's not lost even if config is invalid
|
||||||
|
Loading…
x
Reference in New Issue
Block a user