Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions packages/clipper/src/background/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ import packageInfo from '../../package.json'

const isFirefox = navigator.userAgent.indexOf('Firefox/') !== -1

const openClipperAuthPanel = async (pane: 'sign-in' | 'register') => {
const popupPath = await browserAction.getPopup({})
const url = new URL(popupPath, runtime.getURL('/'))
url.searchParams.set('route', 'extension')
url.searchParams.set('pane', pane)

await windows.create({
type: 'detached_panel',
url: url.toString(),
width: 350,
height: 300,
})
}

const openPopupAndClipSelection = async (payload: ClipPayload) => {
await storage.local.set({ clip: payload })

Expand Down Expand Up @@ -33,6 +47,8 @@ runtime.onMessage.addListener(async (message: RuntimeMessage) => {
return
}
void openPopupAndClipSelection(message.payload)
} else if (message.type === RuntimeMessageTypes.OpenClipperAuthPanel) {
await openClipperAuthPanel(message.pane)
} else if (message.type === RuntimeMessageTypes.CaptureVisibleTab) {
return await tabs.captureVisibleTab(undefined, {
format: 'png',
Expand Down
2 changes: 1 addition & 1 deletion packages/clipper/src/manifest.v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"browser_specific_settings": {
"gecko": {
"id": "{9f917dfe-accd-4d3a-9685-33c3ac0ca643}",
"strict_min_version": "48.0"
"strict_min_version": "150.0"
}
}
}
2 changes: 1 addition & 1 deletion packages/clipper/src/manifest.v3.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"browser_specific_settings": {
"gecko": {
"id": "{9f917dfe-accd-4d3a-9685-33c3ac0ca643}",
"strict_min_version": "48.0"
"strict_min_version": "150.0"
}
}
}
7 changes: 7 additions & 0 deletions packages/clipper/src/types/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const RuntimeMessageTypes = {
ToggleScreenshotMode: 'toggle-screenshot-mode',
CaptureVisibleTab: 'capture-visible-tab',
RunHttpRequest: 'run-http-request',
OpenClipperAuthPanel: 'open-clipper-auth-panel',
} as const

export type RuntimeMessageType = (typeof RuntimeMessageTypes)[keyof typeof RuntimeMessageTypes]
Expand All @@ -33,6 +34,7 @@ export type RuntimeMessageReturnTypes = {
[RuntimeMessageTypes.StartNodeSelection]: void
[RuntimeMessageTypes.ToggleScreenshotMode]: void
[RuntimeMessageTypes.RunHttpRequest]: void
[RuntimeMessageTypes.OpenClipperAuthPanel]: void
}

export type RuntimeMessage =
Expand All @@ -48,11 +50,16 @@ export type RuntimeMessage =
type: typeof RuntimeMessageTypes.ToggleScreenshotMode
enabled: boolean
}
| {
type: typeof RuntimeMessageTypes.OpenClipperAuthPanel
pane: 'sign-in' | 'register'
}
| {
type: Exclude<
RuntimeMessageType,
| MessagesWithClipPayload
| typeof RuntimeMessageTypes.ToggleScreenshotMode
| typeof RuntimeMessageTypes.RunHttpRequest
| typeof RuntimeMessageTypes.OpenClipperAuthPanel
>
}
10 changes: 7 additions & 3 deletions packages/clipper/src/utils/sendMessageToActiveTab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ import { RuntimeMessage, RuntimeMessageReturnTypes } from '../types/message'
export default async function sendMessageToActiveTab<T extends RuntimeMessage>(
message: T,
): Promise<RuntimeMessageReturnTypes[T['type']] | undefined> {
const [activeTab] = await tabs.query({ active: true, currentWindow: true, windowType: 'normal' })
const [activeTab] = await tabs.query({ active: true, lastFocusedWindow: true })

if (!activeTab || !activeTab.id) {
if (!activeTab?.id) {
return
}

return await tabs.sendMessage(activeTab.id, message)
try {
return await tabs.sendMessage(activeTab.id, message)
} catch {
return
}
}
5 changes: 5 additions & 0 deletions packages/snjs/lib/Application/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import {
CreateEncryptedBackupFile,
WebSocketsService,
PreferencesServiceEvent,
AuthenticatorManager,
} from '@standardnotes/services'
import {
SNNote,
Expand Down Expand Up @@ -955,6 +956,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return this.dependencies.get<AddAuthenticator>(TYPES.AddAuthenticator)
}

fetchAuthenticatorRegistrationOptions(): Promise<Record<string, unknown> | null> {
return this.dependencies.get<AuthenticatorManager>(TYPES.AuthenticatorManager).generateRegistrationOptions()
}

get listAuthenticators(): ListAuthenticators {
return this.dependencies.get<ListAuthenticators>(TYPES.ListAuthenticators)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export class AddAuthenticator implements UseCaseInterface<void> {
)
}

const registrationOptions = await this.authenticatorClient.generateRegistrationOptions()
const registrationOptions =
dto.registrationOptions ?? (await this.authenticatorClient.generateRegistrationOptions())
if (registrationOptions === null) {
return Result.fail('Could not generate authenticator registration options')
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export interface AddAuthenticatorDTO {
userUuid: string
authenticatorName: string
/** When provided, skips the server round-trip so WebAuthn can run in the same user gesture. */
registrationOptions?: Record<string, unknown>
}
8 changes: 5 additions & 3 deletions packages/web/src/javascripts/Application/WebApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { action, computed, makeObservable, observable } from 'mobx'
import { startAuthentication, startRegistration } from '@simplewebauthn/browser'
import { PanelResizedData } from '@/Types/PanelResizedData'
import { getBlobFromBase64, isDesktopApplication, isDev } from '@/Utils'
import { prepareWebAuthnRegistrationOptions } from '@/Utils/prepareWebAuthnRegistrationOptions'
import {
ArchiveManager,
AutolockService,
Expand Down Expand Up @@ -128,9 +129,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter
deviceInterface.environment === Environment.Mobile ? 250 : ApplicationOptionsDefaults.sleepBetweenBatches,
allowMultipleSelection: deviceInterface.environment !== Environment.Mobile,
allowNoteSelectionStatePersistence: deviceInterface.environment !== Environment.Mobile,
u2fAuthenticatorRegistrationPromptFunction: startRegistration as unknown as (
registrationOptions: Record<string, unknown>,
) => Promise<Record<string, unknown>>,
u2fAuthenticatorRegistrationPromptFunction: ((registrationOptions: Record<string, unknown>) =>
startRegistration(
prepareWebAuthnRegistrationOptions(registrationOptions) as unknown as Parameters<typeof startRegistration>[0],
)) as unknown as (registrationOptions: Record<string, unknown>) => Promise<Record<string, unknown>>,
u2fAuthenticatorVerificationPromptFunction: startAuthentication as unknown as (
authenticationOptions: Record<string, unknown>,
) => Promise<Record<string, unknown>>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { Username } from '@standardnotes/snjs'
import { Environment, Username } from '@standardnotes/snjs'
import { ChallengePrompt } from '@standardnotes/services'
import { RefObject, useState } from 'react'
import { RefObject, useCallback, useState } from 'react'
import { c } from 'ttag'

import { WebApplication } from '@/Application/WebApplication'
import { IS_FIREFOX } from '@/Components/SuperEditor/Lexical/Shared/environment'

import Button from '../Button/Button'
import Icon from '../Icon/Icon'

import { InputValue } from './InputValue'
import U2FPromptIframeContainer from './U2FPromptIframeContainer'
import U2FPromptFirefoxNative from './U2FPromptFirefoxNative'
import { isAndroid } from '@standardnotes/ui-services'

type Props = {
Expand All @@ -24,11 +26,31 @@ const U2FPrompt = ({ application, onValueChange, prompt, buttonRef, contextData
const [authenticatorResponse, setAuthenticatorResponse] = useState<Record<string, unknown> | null>(null)
const [error, setError] = useState('')

const handleNativeResponse = useCallback(
(response: Record<string, unknown>) => {
onValueChange(response, prompt)
},
[onValueChange, prompt],
)

if (!application.isFullU2FClient && !isAndroid()) {
const apiHost = application.getHost.execute().getValue() || window.defaultSyncServer

if (application.environment === Environment.Clipper && IS_FIREFOX) {
return (
<U2FPromptFirefoxNative
contextData={contextData}
apiHost={apiHost}
buttonRef={buttonRef}
onResponse={handleNativeResponse}
/>
)
}

return (
<U2FPromptIframeContainer
contextData={contextData}
apiHost={application.getHost.execute().getValue() || window.defaultSyncServer}
apiHost={apiHost}
onResponse={(response) => {
onValueChange(response, prompt)
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { log, LoggingDomain } from '@/Logging'
import { getU2fRelyingPartyId } from '@/Constants/U2FConstants'
import { startAuthentication } from '@simplewebauthn/browser'
import type { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/typescript-types'
import { RefObject, useCallback, useState } from 'react'
import { c } from 'ttag'

import Button from '../Button/Button'

type Props = {
contextData?: Record<string, unknown>
onResponse: (response: Record<string, unknown>) => void
apiHost: string
buttonRef: RefObject<HTMLButtonElement>
}

const U2FPromptFirefoxNative = ({ contextData, onResponse, apiHost, buttonRef }: Props) => {
const [pending, setPending] = useState(false)
const [error, setError] = useState('')

const authenticateWithSecurityKey = useCallback(async () => {
const username = (contextData as { username: string } | undefined)?.username
if (!username) {
setError(c('B1.Account.SignIn.Error').t`No username provided`)
return
}

setPending(true)
setError('')

try {
const response = await fetch(`${apiHost}/v1/authenticators/generate-authentication-options`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username }),
})

const jsonResponse = await response.json()
if (!jsonResponse.data?.options) {
throw new Error(c('B1.Account.SignIn.Error').t`No options returned from server`)
}

const options = jsonResponse.data.options as PublicKeyCredentialRequestOptionsJSON

const rpId = getU2fRelyingPartyId(apiHost, options)

log(LoggingDomain.U2F, 'Starting native WebAuthn authentication', {
username,
apiHost,
rpId,
})

const assertionResponse = await startAuthentication({
...options,
rpId,
})

log(LoggingDomain.U2F, 'Native WebAuthn authentication completed', { id: assertionResponse.id })

onResponse(assertionResponse as unknown as Record<string, unknown>)
} catch (authError) {
setError(authError instanceof Error ? authError.message : String(authError))
console.error(authError)
} finally {
setPending(false)
}
}, [contextData, apiHost, onResponse])

return (
<div className="min-w-76">
{error && <div className="text-danger">{error}</div>}
<Button primary fullWidth onClick={authenticateWithSecurityKey} disabled={pending} ref={buttonRef}>
{pending
? c('B1.Account.SignIn.Status').t`Waiting for security key...`
: c('B5.SecuritySync.Challenge.Action').t`Authenticate Device`}
</Button>
</div>
)
}

export default U2FPromptFirefoxNative
Loading
Loading