Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototype manifest v3 extension #2084

Draft
wants to merge 27 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
36a543e
Update webextension-polyfill
lukaw3d Nov 1, 2024
1a0a61f
Remove bg page and migrate to manifest v3 (each popup/tab has own state)
lukaw3d Nov 1, 2024
736928b
iteration 1: Open ext as a single tab
lukaw3d Nov 1, 2024
b826ddb
Badge show open tab
lukaw3d Nov 1, 2024
69263fc
Ext force profile
lukaw3d Nov 1, 2024
9685f6a
iteration 2: Open ext as a single popup
lukaw3d Nov 1, 2024
e5dc53c
iteration 3: Retain encryption key between popup reopenings
lukaw3d Nov 2, 2024
e81a4b0
iteration 4: revert back to non-persistent popup
lukaw3d Nov 2, 2024
6d03d50
TODO: check if ledger popup works in all iterations
lukaw3d Nov 2, 2024
ffa9ad8
Disconnect redux from ConnectDevicePage
lukaw3d Nov 5, 2024
17061b0
fixup! Remove bg page and migrate to manifest v3 (each popup/tab has …
lukaw3d Nov 5, 2024
1fb1a15
Rename ConnectDevicePage to ExtensionRequestLedgerPermissionPopup
lukaw3d Nov 5, 2024
3d5876b
rename component
lukaw3d Nov 5, 2024
7bfe2e2
ExtensionRequestLedgerPermissionPopup as separate entry
lukaw3d Nov 5, 2024
6fcd145
move
lukaw3d Nov 5, 2024
73e0e10
Disconnect redux from theme ExtensionRequestLedgerPermissionPopup (al…
lukaw3d Nov 6, 2024
7671cb9
focus ExtensionRequestLedgerPermissionPopup if it exists when user cl…
lukaw3d Nov 6, 2024
a8f474e
Revert to 1a0a61f12fe9907f85f0f745e3bb25e90bec6d8c
lukaw3d Nov 6, 2024
4a61f46
Retain encryption key between popup reopenings
lukaw3d Nov 6, 2024
3a6223d
Force extension users to create a profile
lukaw3d Nov 1, 2024
c88b2fc
Rename ConnectDevicePage to ExtensionRequestLedgerPermissionPopup
lukaw3d Nov 6, 2024
f180d51
Refactor ExtensionRequestLedgerPermission as html entry without redux
lukaw3d Nov 6, 2024
dffce4d
Remove unused webext-redux
lukaw3d Nov 6, 2024
874365b
TODO rename again
lukaw3d Nov 6, 2024
55d8b8b
Focus ledger access popup twice
lukaw3d Nov 6, 2024
64f8789
revert f180d5180d0a9e2d22b854b37c40252920fa66d2 partially
lukaw3d Nov 6, 2024
aaccd7a
Refactor how extension requests ledger access
lukaw3d Nov 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions extension/src/background.html

This file was deleted.

6 changes: 0 additions & 6 deletions extension/src/background.ts

This file was deleted.

23 changes: 13 additions & 10 deletions extension/src/popup/popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,27 @@ import React from 'react'
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'
import { HelmetProvider } from 'react-helmet-async'
import { Store } from 'webext-redux'
import { configureAppStore } from 'store/configureStore'
import { createHashRouter, RouterProvider } from 'react-router-dom'

import { ThemeProvider } from 'styles/theme/ThemeProvider'
import browser from 'webextension-polyfill'

import 'locales/i18n'
import 'sanitize.css/sanitize.css'
import 'styles/main.css'
import { routes } from './routes'

const container = document.getElementById('root') as HTMLElement
const root = createRoot(container!)
const store = new Store()
const router = createHashRouter(routes)

store.ready().then(() => {
// Open as a single popup:
// - single: so no need to sync between tabs
if (browser.extension.getViews({ type: 'tab' }).length > 0) {
console.log('This is a tab. Close.')
window.close()
} else {
const container = document.getElementById('root') as HTMLElement
const root = createRoot(container!)
const store = configureAppStore()
const router = createHashRouter(routes)
root.render(
<Provider store={store}>
<ThemeProvider>
Expand All @@ -29,6 +34,4 @@ store.ready().then(() => {
</ThemeProvider>
</Provider>,
)
})

console.log('popup')
}
7 changes: 4 additions & 3 deletions internals/getSecurityHeaders.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ const getCsp = ({ isExtension, isDev }) =>
default-src 'none';
script-src
'self'
${isDev ? reactErrorOverlay : ''}
${isDev ? hmrScripts : ''}
'report-sample';
${!isExtension && isDev ? reactErrorOverlay : '' /* Manifest v3 doesn't allow anything */}
${!isExtension && isDev ? hmrScripts : ''}
${!isExtension ? 'report-sample' : ''}
;
style-src
'self'
'unsafe-inline'
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
"typed-redux-saga": "1.5.0",
"valid-url": "1.0.9",
"webext-redux": "2.1.9",
Copy link
Contributor

@buberdds buberdds Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

webext-redux v4 supports manifest v3 and service workers, but looking at PR we will remove this dep completely. Did you encounter any issues when using it? Or did you find that brainstorming the problem without it was simply easier?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to make it simpler and more like our web wallet

webext-redux is needed to sync multiple extension popups and/or communication with dapps. We still don't have dapp communication, and we probably don't need multiple popups.

(we could also sync multiple popups the same way we sync web wallet tabs by enabling

/** Syncing tabs is only needed in web app, not in extension. */
export const needsSyncingTabs = runtimeIs === 'webapp'
)

"webextension-polyfill": "0.10.0"
"webextension-polyfill": "0.12.0"
},
"devDependencies": {
"@capacitor/cli": "6.0.0",
Expand All @@ -123,7 +123,7 @@
"@types/testing-library__jest-dom": "5.14.9",
"@types/valid-url": "1.0.7",
"@types/w3c-web-usb": "1.0.10",
"@types/webextension-polyfill": "0.10.7",
"@types/webextension-polyfill": "0.12.1",
"@typescript-eslint/eslint-plugin": "6.9.1",
"@typescript-eslint/parser": "6.9.1",
"babel-plugin-istanbul": "6.1.1",
Expand Down
13 changes: 5 additions & 8 deletions public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "__MSG_appName__",
"short_name": "__MSG_appName__",
"description": "__MSG_appDescription__",
"manifest_version": 2,
"manifest_version": 3,
"version": "2.0.0",
"default_locale": "en",
"icons": {
Expand All @@ -15,7 +15,7 @@
"128": "./Icon Blue 512.png",
"512": "./Icon Blue 512.png"
},
"browser_action": {
"action": {
"default_icon": {
"16": "./Icon Blue 512.png",
"19": "./Icon Blue 512.png",
Expand All @@ -28,12 +28,9 @@
"default_title": "ROSE Wallet",
"default_popup": "../extension/src/popup.html"
},
"permissions": ["storage", "notifications", "activeTab"],
"content_security_policy": "{{{ EXTENSION_CSP }}}",
"background": {
"page": "../extension/src/background.html",
"persistent": true
"permissions": ["storage", "notifications"],
"content_security_policy": {
"extension_pages": "{{{ EXTENSION_CSP }}}"
},
"web_accessible_resources": ["./oasis-xu-frame.html"],
"externally_connectable": { "ids": [] }
}
1 change: 0 additions & 1 deletion public/oasis-xu-frame.html

This file was deleted.

9 changes: 9 additions & 0 deletions src/app/components/Persist/ChoosePasswordFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ChoosePasswordInputFields } from './ChoosePasswordInputFields'
import { runtimeIs } from 'config'

export function ChoosePasswordFields() {
const { t } = useTranslation()
Expand All @@ -17,6 +18,8 @@ export function ChoosePasswordFields() {
const hasUnpersistedAccounts = unlockedStatus === 'openUnpersisted'
const [startPersisting, setStartPersisting] = useState(!hasUnpersistedAccounts)

const isExtension = runtimeIs === 'extension'

const isChoiceDisabled =
isPersistenceUnsupported ||
unlockedStatus === 'unlockedProfile' ||
Expand All @@ -41,6 +44,12 @@ export function ChoosePasswordFields() {
disabled: true,
checked: unlockedStatus === 'unlockedProfile',
}
: isExtension
? {
disabled: true,
// Force creating a profile in Manifest v3 extension because we can't keep state in memory
checked: true,
}
: {
checked: startPersisting,
})}
Expand Down
1 change: 1 addition & 0 deletions src/app/pages/OpenWalletPage/webextension.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function FromLedgerWebExtension() {
webExtensionUSBLedgerAccess={() => {
navigate('/open-wallet/ledger/usb')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uh I thought our mechanism to request ledger permissions in ext worked completely differently. It requires redux syncing from openLedgerAccessPopup? :O

openLedgerAccessPopup(href)
// check if ledger popup works
}}
/>
)
Expand Down
8 changes: 8 additions & 0 deletions src/app/state/persist/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ export async function decryptWithPassword<T>(
): Promise<T> {
const encryptedObj = fromBase64andParse<EncryptedObject>(encryptedString)
const derivedKeyWithSalt = await deriveKeyFromPassword(password, encryptedObj.salt)
return await decryptWithKey(derivedKeyWithSalt, encryptedString)
}

export async function decryptWithKey<T>(
derivedKeyWithSalt: KeyWithSalt,
encryptedString: EncryptedString<T>,
): Promise<T> {
const encryptedObj = fromBase64andParse<EncryptedObject>(encryptedString)
const dataBytes = nacl.secretbox.open(encryptedObj.secretbox, encryptedObj.nonce, derivedKeyWithSalt.key)
if (!dataBytes) throw new PasswordWrongError()

Expand Down
57 changes: 56 additions & 1 deletion src/app/state/persist/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,26 @@ import { isActionSynced } from 'redux-state-sync'
import { persistActions, STORAGE_FIELD } from './index'
import {
base64andStringify,
decryptWithKey,
decryptWithPassword,
deriveKeyFromPassword,
encryptWithKey,
fromBase64andParse,
} from './encryption'
import { RootState } from 'types'
import { EncryptedString, KeyWithSalt, PersistedRootState, SetUnlockedRootStatePayload } from './types'
import {
EncryptedString,
KeyWithSalt,
PersistState,
PersistedRootState,
SetUnlockedRootStatePayload,
} from './types'
import { PasswordWrongError } from 'types/errors'
import { walletActions } from 'app/state/wallet'
import { selectUnlockedStatus } from 'app/state/selectUnlockedStatus'
import { runtimeIs } from 'config'
import { backupAndDeleteV0ExtProfile, readStorageV0 } from '../../../utils/walletExtensionV0'
import { selectStringifiedEncryptionKey } from './selectors'

function* watchPersistAsync() {
yield* fork(function* () {
Expand Down Expand Up @@ -211,8 +219,55 @@ function* encryptAndPersistState(action: AnyAction) {
window.localStorage.setItem(STORAGE_FIELD, encryptedState)
}

function* retainEncryptionKeyBetweenPopupReopenings() {
if (runtimeIs !== 'extension') return
yield* fork(function* () {
const channelQueue = yield* actionChannel<AnyAction>('*')
let previousStringifiedEncryptionKey: PersistState['stringifiedEncryptionKey'] = undefined
while (true) {
yield* take(channelQueue)
const stringifiedEncryptionKey = yield* select(selectStringifiedEncryptionKey)
if (stringifiedEncryptionKey !== previousStringifiedEncryptionKey) {
previousStringifiedEncryptionKey = stringifiedEncryptionKey
yield* call(writeSharedExtMemory, stringifiedEncryptionKey)
}
}
})

yield* fork(function* () {
const encryptedState = window.localStorage.getItem(
STORAGE_FIELD,
) as EncryptedString<PersistedRootState> | null
if (!encryptedState) return // Ignore
try {
const stringifiedEncryptionKey = yield* call(readSharedExtMemory)
if (!stringifiedEncryptionKey) return // Ignore
if (stringifiedEncryptionKey === 'skipped') return // Ignore
const keyWithSalt: KeyWithSalt = fromBase64andParse(stringifiedEncryptionKey)
const persistedRootState = yield* call(decryptWithKey<PersistedRootState>, keyWithSalt, encryptedState)
yield* put(persistActions.setUnlockedRootState({ persistedRootState, stringifiedEncryptionKey }))
} catch (error) {
// Ignore
}
})
}

async function writeSharedExtMemory(stringifiedEncryptionKey: PersistState['stringifiedEncryptionKey']) {
if (runtimeIs !== 'extension') return
const browser = await import('webextension-polyfill')
await browser.storage.session.set({ stringifiedEncryptionKey })
}

async function readSharedExtMemory() {
if (runtimeIs !== 'extension') return
const browser = await import('webextension-polyfill')
const storage = await browser.storage.session.get('stringifiedEncryptionKey')
return storage.stringifiedEncryptionKey as PersistState['stringifiedEncryptionKey']
}

export function* persistSaga() {
yield* watchPersistAsync()
yield* retainEncryptionKeyBetweenPopupReopenings()
const storageV0 = yield* call(readStorageV0)
yield* put(persistActions.setHasV0StorageToMigrate(!!storageV0?.chromeStorageLocal.keyringData))
}
5 changes: 5 additions & 0 deletions src/app/state/persist/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ export const selectIsPersistenceUnsupported = createSelector(
[selectSlice],
state => state.isPersistenceUnsupported,
)

export const selectStringifiedEncryptionKey = createSelector(
[selectSlice],
state => state.stringifiedEncryptionKey,
)
16 changes: 8 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3194,10 +3194,10 @@
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597"
integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==

"@types/webextension-polyfill@0.10.7":
version "0.10.7"
resolved "https://registry.yarnpkg.com/@types/webextension-polyfill/-/webextension-polyfill-0.10.7.tgz#de059250599733a60ed26c8a0c81e21e11183b90"
integrity sha512-10ql7A0qzBmFB+F+qAke/nP1PIonS0TXZAOMVOxEUsm+lGSW6uwVcISFNa0I4Oyj0884TZVWGGMIWeXOVSNFHw==
"@types/webextension-polyfill@0.12.1":
version "0.12.1"
resolved "https://registry.yarnpkg.com/@types/webextension-polyfill/-/webextension-polyfill-0.12.1.tgz#8dae244fe094cbb541005362e8e22f16671f6054"
integrity sha512-xPTFWwQ8BxPevPF2IKsf4hpZNss4LxaOLZXypQH4E63BDLmcwX/RMGdI4tB4VO4Nb6xDBH3F/p4gz4wvof1o9w==

"@types/yargs-parser@*":
version "20.2.0"
Expand Down Expand Up @@ -9954,10 +9954,10 @@ [email protected]:
lodash.assignin "^4.2.0"
lodash.clonedeep "^4.5.0"

webextension-polyfill@0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz#ccb28101c910ba8cf955f7e6a263e662d744dbb8"
integrity sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g==
webextension-polyfill@0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.12.0.tgz#f62c57d2cd42524e9fbdcee494c034cae34a3d69"
integrity sha512-97TBmpoWJEE+3nFBQ4VocyCdLKfw54rFaJ6EVQYLBCXqCIpLSZkwGgASpv4oPt9gdKCJ80RJlcmNzNn008Ag6Q==

webidl-conversions@^7.0.0:
version "7.0.0"
Expand Down
Loading