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 WalletLogsContext = createContext()
|
||||
|
||||
const initIndexedDB = async (storeName) => {
|
||||
const initIndexedDB = async (dbName, storeName) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!window.indexedDB) {
|
||||
return reject(new Error('IndexedDB not supported'))
|
||||
}
|
||||
|
||||
// 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
|
||||
request.onupgradeneeded = () => {
|
||||
|
@ -159,7 +159,12 @@ const initIndexedDB = async (storeName) => {
|
|||
}
|
||||
|
||||
const WalletLoggerProvider = ({ children }) => {
|
||||
const me = useMe()
|
||||
const [logs, setLogs] = useState([])
|
||||
let dbName = 'app:storage'
|
||||
if (me) {
|
||||
dbName = `${dbName}:${me.id}`
|
||||
}
|
||||
const idbStoreName = 'wallet_logs'
|
||||
const idb = useRef()
|
||||
const logQueue = useRef([])
|
||||
|
@ -211,7 +216,7 @@ const WalletLoggerProvider = ({ children }) => {
|
|||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
initIndexedDB(idbStoreName)
|
||||
initIndexedDB(dbName, idbStoreName)
|
||||
.then(db => {
|
||||
idb.current = db
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import classNames from 'classnames'
|
|||
import SnIcon from '@/svgs/sn.svg'
|
||||
import { useHasNewNotes } from '../use-has-new-notes'
|
||||
import { useWalletLogger } from '../logger'
|
||||
import { useWebLNConfigurator } from '../webln'
|
||||
|
||||
export function Brand ({ className }) {
|
||||
return (
|
||||
|
@ -162,8 +163,6 @@ export function NavWalletSummary ({ className }) {
|
|||
|
||||
export function MeDropdown ({ me, dropNavKey }) {
|
||||
if (!me) return null
|
||||
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
||||
const { deleteLogs } = useWalletLogger()
|
||||
return (
|
||||
<div className='position-relative'>
|
||||
<Dropdown className={styles.dropdown} align='end'>
|
||||
|
@ -202,22 +201,7 @@ export function MeDropdown ({ me, dropNavKey }) {
|
|||
</Link>
|
||||
</div>
|
||||
<Dropdown.Divider />
|
||||
<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) {
|
||||
// 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>
|
||||
<LogoutDropdownItem />
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
{!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 () {
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -2,9 +2,7 @@ import { useState } from 'react'
|
|||
import { Dropdown, Image, Nav, Navbar, Offcanvas } from 'react-bootstrap'
|
||||
import { MEDIA_URL } from '@/lib/constants'
|
||||
import Link from 'next/link'
|
||||
import { useServiceWorker } from '@/components/serviceworker'
|
||||
import { signOut } from 'next-auth/react'
|
||||
import { LoginButtons, NavWalletSummary } from '../common'
|
||||
import { LoginButtons, LogoutDropdownItem, NavWalletSummary } from '../common'
|
||||
import AnonIcon from '@/svgs/spy-fill.svg'
|
||||
import styles from './footer.module.css'
|
||||
import classNames from 'classnames'
|
||||
|
@ -14,7 +12,6 @@ export default function OffCanvas ({ me, dropNavKey }) {
|
|||
|
||||
const handleClose = () => setShow(false)
|
||||
const handleShow = () => setShow(true)
|
||||
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
||||
|
||||
const MeImage = ({ onClick }) => me
|
||||
? (
|
||||
|
@ -78,22 +75,7 @@ export default function OffCanvas ({ me, dropNavKey }) {
|
|||
</Link>
|
||||
</div>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item
|
||||
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>
|
||||
<LogoutDropdownItem />
|
||||
</>
|
||||
)
|
||||
: <LoginButtons />}
|
||||
|
|
|
@ -33,6 +33,15 @@ export const Status = {
|
|||
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 }) {
|
||||
const lnbits = useLNbits()
|
||||
const nwc = useNWC()
|
||||
|
@ -114,8 +123,14 @@ function RawWebLNProvider ({ children }) {
|
|||
})
|
||||
}, [setEnabledProviders])
|
||||
|
||||
const clearConfig = useCallback(async () => {
|
||||
lnbits.clearConfig()
|
||||
nwc.clearConfig()
|
||||
await lnc.clearConfig()
|
||||
}, [])
|
||||
|
||||
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}
|
||||
</WebLNContext.Provider>
|
||||
)
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||
import { useWalletLogger } from '../logger'
|
||||
import { Status } from '.'
|
||||
import { Status, migrateLocalStorage } from '.'
|
||||
import { bolt11Tags } from '@/lib/bolt11'
|
||||
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
|
||||
|
||||
|
@ -65,13 +66,17 @@ const getPayment = async (baseUrl, adminKey, paymentHash) => {
|
|||
}
|
||||
|
||||
export function LNbitsProvider ({ children }) {
|
||||
const me = useMe()
|
||||
const [url, setUrl] = useState('')
|
||||
const [adminKey, setAdminKey] = useState('')
|
||||
const [status, setStatus] = useState()
|
||||
const { logger } = useWalletLogger(Wallet.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 response = await getWallet(url, adminKey)
|
||||
|
@ -110,11 +115,18 @@ export function LNbitsProvider ({ children }) {
|
|||
}, [logger, url, adminKey])
|
||||
|
||||
const loadConfig = useCallback(async () => {
|
||||
const configStr = window.localStorage.getItem(storageKey)
|
||||
let configStr = window.localStorage.getItem(storageKey)
|
||||
setStatus(Status.Initialized)
|
||||
if (!configStr) {
|
||||
logger.info('no existing config found')
|
||||
return
|
||||
if (me) {
|
||||
// backwards compatibility: try old storageKey
|
||||
const oldStorageKey = storageKey.split(':').slice(0, -1).join(':')
|
||||
configStr = migrateLocalStorage(oldStorageKey, storageKey)
|
||||
}
|
||||
if (!configStr) {
|
||||
logger.info('no existing config found')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const config = JSON.parse(configStr)
|
||||
|
@ -141,7 +153,7 @@ export function LNbitsProvider ({ children }) {
|
|||
logger.info('wallet disabled')
|
||||
throw err
|
||||
}
|
||||
}, [logger])
|
||||
}, [me, logger])
|
||||
|
||||
const saveConfig = useCallback(async (config) => {
|
||||
// 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 { useWalletLogger } from '../logger'
|
||||
import LNC from '@lightninglabs/lnc-web'
|
||||
import { Status } from '.'
|
||||
import { Status, migrateLocalStorage } from '.'
|
||||
import { bolt11Tags } from '@/lib/bolt11'
|
||||
import useModal from '../modal'
|
||||
import { Form, PasswordInput, SubmitButton } from '../form'
|
||||
import CancelButton from '../cancel-button'
|
||||
import { Mutex } from 'async-mutex'
|
||||
import { Wallet } from '@/lib/constants'
|
||||
import { useMe } from '../me'
|
||||
|
||||
const LNCContext = createContext()
|
||||
const mutex = new Mutex()
|
||||
|
||||
async function getLNC () {
|
||||
async function getLNC ({ me }) {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -33,6 +36,7 @@ function validateNarrowPerms (lnc) {
|
|||
}
|
||||
|
||||
export function LNCProvider ({ children }) {
|
||||
const me = useMe()
|
||||
const { logger } = useWalletLogger(Wallet.LNC)
|
||||
const [config, setConfig] = useState({})
|
||||
const [lnc, setLNC] = useState()
|
||||
|
@ -165,7 +169,7 @@ export function LNCProvider ({ children }) {
|
|||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const lnc = await getLNC()
|
||||
const lnc = await getLNC({ me })
|
||||
setLNC(lnc)
|
||||
setStatus(Status.Initialized)
|
||||
if (lnc.credentials.isPaired) {
|
||||
|
@ -185,7 +189,7 @@ export function LNCProvider ({ children }) {
|
|||
setStatus(Status.Error)
|
||||
}
|
||||
})()
|
||||
}, [setStatus, setConfig, logger])
|
||||
}, [me, setStatus, setConfig, logger])
|
||||
|
||||
return (
|
||||
<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 { parseNwcUrl } from '@/lib/url'
|
||||
import { useWalletLogger } from '../logger'
|
||||
import { Status } from '.'
|
||||
import { Status, migrateLocalStorage } from '.'
|
||||
import { bolt11Tags } from '@/lib/bolt11'
|
||||
import { Wallet } from '@/lib/constants'
|
||||
import { useMe } from '../me'
|
||||
|
||||
const NWCContext = createContext()
|
||||
|
||||
export function NWCProvider ({ children }) {
|
||||
const me = useMe()
|
||||
const [nwcUrl, setNwcUrl] = useState('')
|
||||
const [walletPubkey, setWalletPubkey] = useState()
|
||||
const [relayUrl, setRelayUrl] = useState()
|
||||
|
@ -19,7 +21,10 @@ export function NWCProvider ({ children }) {
|
|||
const { logger } = useWalletLogger(Wallet.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) => {
|
||||
logger.info(`requesting info event from ${relayUrl}`)
|
||||
|
@ -97,11 +102,18 @@ export function NWCProvider ({ children }) {
|
|||
}, [logger])
|
||||
|
||||
const loadConfig = useCallback(async () => {
|
||||
const configStr = window.localStorage.getItem(storageKey)
|
||||
let configStr = window.localStorage.getItem(storageKey)
|
||||
setStatus(Status.Initialized)
|
||||
if (!configStr) {
|
||||
logger.info('no existing config found')
|
||||
return
|
||||
if (me) {
|
||||
// backwards compatibility: try old storageKey
|
||||
const oldStorageKey = storageKey.split(':').slice(0, -1).join(':')
|
||||
configStr = migrateLocalStorage(oldStorageKey, storageKey)
|
||||
}
|
||||
if (!configStr) {
|
||||
logger.info('no existing config found')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const config = JSON.parse(configStr)
|
||||
|
@ -130,7 +142,7 @@ export function NWCProvider ({ children }) {
|
|||
logger.info('wallet disabled')
|
||||
throw err
|
||||
}
|
||||
}, [validateParams, logger])
|
||||
}, [me, validateParams, logger])
|
||||
|
||||
const saveConfig = useCallback(async (config) => {
|
||||
// immediately store config so it's not lost even if config is invalid
|
||||
|
|
Loading…
Reference in New Issue