From b3bbc9e6425cf158ac9a73c0109a88f8b501f26d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Thu, 13 Apr 2023 09:17:06 +0200 Subject: [PATCH 1/2] feat(mobile): add U2F support for Android devices --- ...-core-npm-1.12.0-c4ef3cb073-852f15e481.zip | Bin 0 -> 45299 bytes packages/api/package.json | 2 +- packages/features/package.json | 2 +- packages/mobile/android/app/build.gradle | 1 + .../com/standardnotes/Fido2ApiModule.java | 203 ++++++++++++++++++ .../com/standardnotes/Fido2ApiPackage.java | 26 +++ .../com/standardnotes/MainApplication.java | 2 + packages/mobile/android/gradle.properties | 5 +- packages/mobile/src/Lib/MobileDevice.ts | 21 ++ packages/services/package.json | 2 +- .../Domain/Device/MobileDeviceInterface.ts | 4 +- packages/snjs/lib/Application/Application.ts | 10 +- ...AuthenticatorAuthenticationOptions.spec.ts | 42 ++++ .../GetAuthenticatorAuthenticationOptions.ts | 23 ++ ...etAuthenticatorAuthenticationOptionsDTO.ts | 3 + ...uthenticatorAuthenticationResponse.spec.ts | 29 +-- .../GetAuthenticatorAuthenticationResponse.ts | 21 +- .../UseCase/UseCaseContainerInterface.ts | 2 + packages/snjs/lib/Domain/index.ts | 2 + packages/snjs/package.json | 2 +- .../GoogleKeepConverter.spec.ts | 2 +- .../Components/ChallengeModal/U2FPrompt.tsx | 106 +++++---- yarn.lock | 17 +- 23 files changed, 445 insertions(+), 82 deletions(-) create mode 100644 .yarn/cache/@standardnotes-domain-core-npm-1.12.0-c4ef3cb073-852f15e481.zip create mode 100644 packages/mobile/android/app/src/main/java/com/standardnotes/Fido2ApiModule.java create mode 100644 packages/mobile/android/app/src/main/java/com/standardnotes/Fido2ApiPackage.java create mode 100644 packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions.spec.ts create mode 100644 packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions.ts create mode 100644 packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptionsDTO.ts diff --git a/.yarn/cache/@standardnotes-domain-core-npm-1.12.0-c4ef3cb073-852f15e481.zip b/.yarn/cache/@standardnotes-domain-core-npm-1.12.0-c4ef3cb073-852f15e481.zip new file mode 100644 index 0000000000000000000000000000000000000000..7f3299643025e380f7aa3b84dfd0566ee66f1f42 GIT binary patch literal 45299 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 O|AbHhzn~t9Gyew allowedKeys = new ArrayList(); + + JSONArray allowedCredentials = authenticationOptions.getJSONArray("allowCredentials"); + for (int i = 0, size = allowedCredentials.length(); i < size; i++) { + JSONObject allowedCredential = allowedCredentials.getJSONObject(i); + allowedKeys.add( + new PublicKeyCredentialDescriptor( + PublicKeyCredentialType.PUBLIC_KEY.toString(), + this.convertBase64URLStringToBytes(allowedCredential.getString("id")), + null + ) + ); + } + + String challenge = authenticationOptions.getString("challenge"); + Double timeout = authenticationOptions.getDouble("timeout") / 1000d; + + PublicKeyCredentialRequestOptions.Builder optionsBuilder = new PublicKeyCredentialRequestOptions + .Builder() + .setRpId(RP_ID) + .setAllowList(allowedKeys) + .setChallenge(this.convertBase64URLStringToBytes(challenge)) + .setTimeoutSeconds(timeout); + + PublicKeyCredentialRequestOptions options = optionsBuilder.build(); + + Task result = this.fido2ApiClient.getSignPendingIntent(options); + + final Activity activity = this.reactContext.getCurrentActivity(); + + result.addOnSuccessListener( + new OnSuccessListener() { + @Override + public void onSuccess(PendingIntent fido2PendingIntent) { + if (fido2PendingIntent == null) { + Log.e(LOGS_TAG, "No pending FIDO intent returned"); + return; + } + + try { + activity.startIntentSenderForResult( + fido2PendingIntent.getIntentSender(), + SIGN_REQUEST_CODE, + null, + 0, + 0, + 0 + ); + } catch (IntentSender.SendIntentException exception) { + Log.e(LOGS_TAG, "Error starting FIDO intent: " + exception.getMessage()); + } + } + } + ); + + result.addOnFailureListener( + new OnFailureListener() { + @Override + public void onFailure(Exception e) { + Log.e(LOGS_TAG, "Error getting FIDO intent: " + e.getMessage()); + signInPromise.reject(e.getMessage()); + } + } + ); + } + + private byte[] convertBase64URLStringToBytes(String base64URLString) { + String base64String = base64URLString.replace('-', '+').replace('_', '/'); + int padding = (4 - (base64String.length() % 4)) % 4; + for (int i = 0; i < padding; i++) { + base64String += '='; + } + + return Base64.decode(base64String, Base64.DEFAULT); + } +} diff --git a/packages/mobile/android/app/src/main/java/com/standardnotes/Fido2ApiPackage.java b/packages/mobile/android/app/src/main/java/com/standardnotes/Fido2ApiPackage.java new file mode 100644 index 00000000000..fdc54a46b13 --- /dev/null +++ b/packages/mobile/android/app/src/main/java/com/standardnotes/Fido2ApiPackage.java @@ -0,0 +1,26 @@ +package com.standardnotes; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Fido2ApiPackage implements ReactPackage { + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + + modules.add(new Fido2ApiModule(reactContext)); + + return modules; + } +} diff --git a/packages/mobile/android/app/src/main/java/com/standardnotes/MainApplication.java b/packages/mobile/android/app/src/main/java/com/standardnotes/MainApplication.java index 11da2f3dcb6..fd6570cc75c 100644 --- a/packages/mobile/android/app/src/main/java/com/standardnotes/MainApplication.java +++ b/packages/mobile/android/app/src/main/java/com/standardnotes/MainApplication.java @@ -37,6 +37,8 @@ protected List getPackages() { @SuppressWarnings("UnnecessaryLocalVariable") List packages = new PackageList(this).getPackages(); + packages.add(new Fido2ApiPackage()); + return packages; } diff --git a/packages/mobile/android/gradle.properties b/packages/mobile/android/gradle.properties index bd233cc3803..2642fcf3b0c 100644 --- a/packages/mobile/android/gradle.properties +++ b/packages/mobile/android/gradle.properties @@ -43,4 +43,7 @@ newArchEnabled=false hermesEnabled=true # Set AsyncStorage limit -AsyncStorage_db_size_in_MB=50 \ No newline at end of file +AsyncStorage_db_size_in_MB=50 + +# The URL of the server +host=https://app.standardnotes.com diff --git a/packages/mobile/src/Lib/MobileDevice.ts b/packages/mobile/src/Lib/MobileDevice.ts index 37ed3e77f41..da473e474dc 100644 --- a/packages/mobile/src/Lib/MobileDevice.ts +++ b/packages/mobile/src/Lib/MobileDevice.ts @@ -22,6 +22,7 @@ import { AppStateStatus, ColorSchemeName, Linking, + NativeModules, PermissionsAndroid, Platform, StatusBar, @@ -71,6 +72,26 @@ export class MobileDevice implements MobileDeviceInterface { private colorSchemeService?: ColorSchemeObserverService, ) {} + async authenticateWithU2F(authenticationOptionsJSONString: string): Promise | null> { + const { Fido2ApiModule } = NativeModules + + if (!Fido2ApiModule) { + this.consoleLog('Fido2ApiModule is not available') + + return null + } + + try { + const response = await Fido2ApiModule.promptForU2FAuthentication(authenticationOptionsJSONString) + + return response + } catch (error) { + this.consoleLog(`Fido2ApiModule.authenticateWithU2F error: ${(error as Error).message}`) + + return null + } + } + purchaseSubscriptionIAP(plan: AppleIAPProductId): Promise { return PurchaseManager.getInstance().purchase(plan) } diff --git a/packages/services/package.json b/packages/services/package.json index 15a445c5e80..52ab46bd224 100644 --- a/packages/services/package.json +++ b/packages/services/package.json @@ -18,7 +18,7 @@ "dependencies": { "@standardnotes/api": "workspace:^", "@standardnotes/common": "^1.46.4", - "@standardnotes/domain-core": "^1.11.3", + "@standardnotes/domain-core": "^1.12.0", "@standardnotes/encryption": "workspace:^", "@standardnotes/files": "workspace:^", "@standardnotes/models": "workspace:^", diff --git a/packages/services/src/Domain/Device/MobileDeviceInterface.ts b/packages/services/src/Domain/Device/MobileDeviceInterface.ts index d46efc75e92..022435cdfd6 100644 --- a/packages/services/src/Domain/Device/MobileDeviceInterface.ts +++ b/packages/services/src/Domain/Device/MobileDeviceInterface.ts @@ -1,6 +1,7 @@ +import { Environment, Platform, RawKeychainValue } from '@standardnotes/models' + import { AppleIAPProductId } from './../Subscription/AppleIAPProductId' import { DeviceInterface } from './DeviceInterface' -import { Environment, Platform, RawKeychainValue } from '@standardnotes/models' import { AppleIAPReceipt } from '../Subscription/AppleIAPReceipt' export interface MobileDeviceInterface extends DeviceInterface { @@ -25,4 +26,5 @@ export interface MobileDeviceInterface extends DeviceInterface { getAppState(): Promise<'active' | 'background' | 'inactive' | 'unknown' | 'extension'> getColorScheme(): Promise<'light' | 'dark' | null | undefined> purchaseSubscriptionIAP(plan: AppleIAPProductId): Promise + authenticateWithU2F(authenticationOptionsJSONString: string): Promise | null> } diff --git a/packages/snjs/lib/Application/Application.ts b/packages/snjs/lib/Application/Application.ts index a437fcac328..4f048895601 100644 --- a/packages/snjs/lib/Application/Application.ts +++ b/packages/snjs/lib/Application/Application.ts @@ -102,6 +102,7 @@ import { ListRevisions } from '@Lib/Domain/UseCase/ListRevisions/ListRevisions' import { GetRevision } from '@Lib/Domain/UseCase/GetRevision/GetRevision' import { DeleteRevision } from '@Lib/Domain/UseCase/DeleteRevision/DeleteRevision' import { GetAuthenticatorAuthenticationResponse } from '@Lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse' +import { GetAuthenticatorAuthenticationOptions } from '@Lib/Domain/UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions' /** How often to automatically sync, in milliseconds */ const DEFAULT_AUTO_SYNC_INTERVAL = 30_000 @@ -182,6 +183,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli private declare _addAuthenticator: AddAuthenticator private declare _listAuthenticators: ListAuthenticators private declare _deleteAuthenticator: DeleteAuthenticator + private declare _getAuthenticatorAuthenticationOptions: GetAuthenticatorAuthenticationOptions private declare _getAuthenticatorAuthenticationResponse: GetAuthenticatorAuthenticationResponse private declare _listRevisions: ListRevisions private declare _getRevision: GetRevision @@ -284,6 +286,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return this._deleteAuthenticator } + get getAuthenticatorAuthenticationOptions(): GetAuthenticatorAuthenticationOptions { + return this._getAuthenticatorAuthenticationOptions + } + get getAuthenticatorAuthenticationResponse(): GetAuthenticatorAuthenticationResponse { return this._getAuthenticatorAuthenticationResponse } @@ -1819,8 +1825,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this._deleteAuthenticator = new DeleteAuthenticator(this.authenticatorManager) + this._getAuthenticatorAuthenticationOptions = new GetAuthenticatorAuthenticationOptions(this.authenticatorManager) + this._getAuthenticatorAuthenticationResponse = new GetAuthenticatorAuthenticationResponse( - this.authenticatorManager, + this._getAuthenticatorAuthenticationOptions, this.options.u2fAuthenticatorVerificationPromptFunction, ) diff --git a/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions.spec.ts b/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions.spec.ts new file mode 100644 index 00000000000..8c1e1a1776e --- /dev/null +++ b/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions.spec.ts @@ -0,0 +1,42 @@ +import { AuthenticatorClientInterface } from '@standardnotes/services' + +import { GetAuthenticatorAuthenticationOptions } from './GetAuthenticatorAuthenticationOptions' + +describe('GetAuthenticatorAuthenticationOptions', () => { + let authenticatorClient: AuthenticatorClientInterface + + const createUseCase = () => new GetAuthenticatorAuthenticationOptions(authenticatorClient) + + beforeEach(() => { + authenticatorClient = {} as jest.Mocked + authenticatorClient.generateAuthenticationOptions = jest.fn().mockResolvedValue({ foo: 'bar' }) + }) + + it('should return an error if username is not provided', async () => { + const result = await createUseCase().execute({ + username: '', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toBe('Could not generate authenticator authentication options: Username cannot be empty') + }) + + it('should return an error if authenticator client fails to generate authentication options', async () => { + authenticatorClient.generateAuthenticationOptions = jest.fn().mockResolvedValue(null) + + const result = await createUseCase().execute({ + username: 'test@test.te', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toBe('Could not generate authenticator authentication options') + }) + + it('should return ok if authenticator client succeeds to generate authenticator response', async () => { + const result = await createUseCase().execute({ + username: 'test@test.te', + }) + + expect(result.isFailed()).toBe(false) + }) +}) diff --git a/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions.ts b/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions.ts new file mode 100644 index 00000000000..7c4210665a6 --- /dev/null +++ b/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions.ts @@ -0,0 +1,23 @@ +import { AuthenticatorClientInterface } from '@standardnotes/services' +import { Result, UseCaseInterface, Username } from '@standardnotes/domain-core' + +import { GetAuthenticatorAuthenticationOptionsDTO } from './GetAuthenticatorAuthenticationOptionsDTO' + +export class GetAuthenticatorAuthenticationOptions implements UseCaseInterface> { + constructor(private authenticatorClient: AuthenticatorClientInterface) {} + + async execute(dto: GetAuthenticatorAuthenticationOptionsDTO): Promise>> { + const usernameOrError = Username.create(dto.username) + if (usernameOrError.isFailed()) { + return Result.fail(`Could not generate authenticator authentication options: ${usernameOrError.getError()}`) + } + const username = usernameOrError.getValue() + + const authenticationOptions = await this.authenticatorClient.generateAuthenticationOptions(username) + if (authenticationOptions === null) { + return Result.fail('Could not generate authenticator authentication options') + } + + return Result.ok(authenticationOptions) + } +} diff --git a/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptionsDTO.ts b/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptionsDTO.ts new file mode 100644 index 00000000000..55886823b90 --- /dev/null +++ b/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptionsDTO.ts @@ -0,0 +1,3 @@ +export interface GetAuthenticatorAuthenticationOptionsDTO { + username: string +} diff --git a/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse.spec.ts b/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse.spec.ts index 882e7a9b9c4..37b08e4cfad 100644 --- a/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse.spec.ts +++ b/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse.spec.ts @@ -1,40 +1,31 @@ -import { AuthenticatorClientInterface } from '@standardnotes/services' - import { GetAuthenticatorAuthenticationResponse } from './GetAuthenticatorAuthenticationResponse' +import { GetAuthenticatorAuthenticationOptions } from '../GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions' +import { Result } from '@standardnotes/domain-core' describe('GetAuthenticatorAuthenticationResponse', () => { - let authenticatorClient: AuthenticatorClientInterface + let getAuthenticatorAuthenticationOptions: GetAuthenticatorAuthenticationOptions let authenticatorVerificationPromptFunction: ( authenticationOptions: Record, ) => Promise> - const createUseCase = () => new GetAuthenticatorAuthenticationResponse(authenticatorClient, authenticatorVerificationPromptFunction) + const createUseCase = () => new GetAuthenticatorAuthenticationResponse(getAuthenticatorAuthenticationOptions, authenticatorVerificationPromptFunction) beforeEach(() => { - authenticatorClient = {} as jest.Mocked - authenticatorClient.generateAuthenticationOptions = jest.fn().mockResolvedValue({ foo: 'bar' }) + getAuthenticatorAuthenticationOptions = {} as jest.Mocked + getAuthenticatorAuthenticationOptions.execute = jest.fn().mockResolvedValue({ foo: 'bar' }) authenticatorVerificationPromptFunction = jest.fn() }) - it('should return an error if username is not provided', async () => { - const result = await createUseCase().execute({ - username: '', - }) - - expect(result.isFailed()).toBe(true) - expect(result.getError()).toBe('Could not generate authenticator authentication options: Username cannot be empty') - }) - - it('should return an error if authenticator client fails to generate authentication options', async () => { - authenticatorClient.generateAuthenticationOptions = jest.fn().mockResolvedValue(null) + it('should return an error if it fails to generate authentication options', async () => { + getAuthenticatorAuthenticationOptions.execute = jest.fn().mockReturnValue(Result.fail('error')) const result = await createUseCase().execute({ username: 'test@test.te', }) expect(result.isFailed()).toBe(true) - expect(result.getError()).toBe('Could not generate authenticator authentication options') + expect(result.getError()).toBe('error') }) it('should return an error if authenticator verification prompt function fails', async () => { @@ -57,7 +48,7 @@ describe('GetAuthenticatorAuthenticationResponse', () => { }) it('should return error if authenticatorVerificationPromptFunction is not provided', async () => { - const result = await new GetAuthenticatorAuthenticationResponse(authenticatorClient).execute({ + const result = await new GetAuthenticatorAuthenticationResponse(getAuthenticatorAuthenticationOptions).execute({ username: 'test@test.te', }) diff --git a/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse.ts b/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse.ts index fad5fe6e155..6f3a102be65 100644 --- a/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse.ts +++ b/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse.ts @@ -1,10 +1,10 @@ -import { AuthenticatorClientInterface } from '@standardnotes/services' -import { Result, UseCaseInterface, Username } from '@standardnotes/domain-core' +import { Result, UseCaseInterface } from '@standardnotes/domain-core' import { GetAuthenticatorAuthenticationResponseDTO } from './GetAuthenticatorAuthenticationResponseDTO' +import { GetAuthenticatorAuthenticationOptions } from '../GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions' export class GetAuthenticatorAuthenticationResponse implements UseCaseInterface> { constructor( - private authenticatorClient: AuthenticatorClientInterface, + private getAuthenticatorAuthenticationOptions: GetAuthenticatorAuthenticationOptions, private authenticatorVerificationPromptFunction?: ( authenticationOptions: Record, ) => Promise>, @@ -17,16 +17,13 @@ export class GetAuthenticatorAuthenticationResponse implements UseCaseInterface< ) } - const usernameOrError = Username.create(dto.username) - if (usernameOrError.isFailed()) { - return Result.fail(`Could not generate authenticator authentication options: ${usernameOrError.getError()}`) - } - const username = usernameOrError.getValue() - - const authenticationOptions = await this.authenticatorClient.generateAuthenticationOptions(username) - if (authenticationOptions === null) { - return Result.fail('Could not generate authenticator authentication options') + const authenticationOptionsOrError = await this.getAuthenticatorAuthenticationOptions.execute({ + username: dto.username, + }) + if (authenticationOptionsOrError.isFailed()) { + return Result.fail(authenticationOptionsOrError.getError()) } + const authenticationOptions = authenticationOptionsOrError.getValue() let authenticatorResponse try { diff --git a/packages/snjs/lib/Domain/UseCase/UseCaseContainerInterface.ts b/packages/snjs/lib/Domain/UseCase/UseCaseContainerInterface.ts index 06c72a5a4cd..25cda4e2794 100644 --- a/packages/snjs/lib/Domain/UseCase/UseCaseContainerInterface.ts +++ b/packages/snjs/lib/Domain/UseCase/UseCaseContainerInterface.ts @@ -7,6 +7,7 @@ import { GetAuthenticatorAuthenticationResponse } from './GetAuthenticatorAuthen import { ListRevisions } from './ListRevisions/ListRevisions' import { GetRevision } from './GetRevision/GetRevision' import { DeleteRevision } from './DeleteRevision/DeleteRevision' +import { GetAuthenticatorAuthenticationOptions } from './GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions' export interface UseCaseContainerInterface { get signInWithRecoveryCodes(): SignInWithRecoveryCodes @@ -15,6 +16,7 @@ export interface UseCaseContainerInterface { get listAuthenticators(): ListAuthenticators get deleteAuthenticator(): DeleteAuthenticator get getAuthenticatorAuthenticationResponse(): GetAuthenticatorAuthenticationResponse + get getAuthenticatorAuthenticationOptions(): GetAuthenticatorAuthenticationOptions get listRevisions(): ListRevisions get getRevision(): GetRevision get deleteRevision(): DeleteRevision diff --git a/packages/snjs/lib/Domain/index.ts b/packages/snjs/lib/Domain/index.ts index d1fe689aa1c..46d068f2811 100644 --- a/packages/snjs/lib/Domain/index.ts +++ b/packages/snjs/lib/Domain/index.ts @@ -6,6 +6,8 @@ export * from './UseCase/DeleteAuthenticator/DeleteAuthenticator' export * from './UseCase/DeleteAuthenticator/DeleteAuthenticatorDTO' export * from './UseCase/DeleteRevision/DeleteRevision' export * from './UseCase/DeleteRevision/DeleteRevisionDTO' +export * from './UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions' +export * from './UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptionsDTO' export * from './UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse' export * from './UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponseDTO' export * from './UseCase/GetRecoveryCodes/GetRecoveryCodes' diff --git a/packages/snjs/package.json b/packages/snjs/package.json index 66bd5943f97..e3ef80ae2b4 100644 --- a/packages/snjs/package.json +++ b/packages/snjs/package.json @@ -37,7 +37,7 @@ "@babel/preset-env": "*", "@standardnotes/api": "workspace:*", "@standardnotes/common": "^1.46.6", - "@standardnotes/domain-core": "^1.11.3", + "@standardnotes/domain-core": "^1.12.0", "@standardnotes/domain-events": "^2.108.1", "@standardnotes/encryption": "workspace:*", "@standardnotes/features": "workspace:*", diff --git a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts index 7923e6d84c8..632b9c4df8a 100644 --- a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts +++ b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts @@ -2,7 +2,7 @@ * @jest-environment jsdom */ -import { WebApplicationInterface } from '@standardnotes/snjs/dist/@types' +import { WebApplicationInterface } from '@standardnotes/snjs' import { jsonTestData, htmlTestData } from './testData' import { GoogleKeepConverter } from './GoogleKeepConverter' diff --git a/packages/web/src/javascripts/Components/ChallengeModal/U2FPrompt.tsx b/packages/web/src/javascripts/Components/ChallengeModal/U2FPrompt.tsx index c62bd31c02a..c74c9ee71b8 100644 --- a/packages/web/src/javascripts/Components/ChallengeModal/U2FPrompt.tsx +++ b/packages/web/src/javascripts/Components/ChallengeModal/U2FPrompt.tsx @@ -1,8 +1,13 @@ -import { WebApplication } from '@/Application/Application' +import { Username } from '@standardnotes/snjs' import { ChallengePrompt } from '@standardnotes/services' import { RefObject, useState } from 'react' + +import { WebApplication } from '@/Application/Application' +import { isAndroid } from '@/Utils' + import Button from '../Button/Button' import Icon from '../Icon/Icon' + import { InputValue } from './InputValue' import U2FPromptIframeContainer from './U2FPromptIframeContainer' @@ -18,7 +23,7 @@ const U2FPrompt = ({ application, onValueChange, prompt, buttonRef, contextData const [authenticatorResponse, setAuthenticatorResponse] = useState | null>(null) const [error, setError] = useState('') - if (!application.isFullU2FClient) { + if (!application.isFullU2FClient && !isAndroid()) { return ( ) - } + } else { + return ( +
+ {error &&
{error}
} + -
- ) + if (authenticatorResponse === null) { + setError('Failed to obtain device response') + return + } + + setAuthenticatorResponse(authenticatorResponse) + onValueChange(authenticatorResponse, prompt) + }} + ref={buttonRef} + > + {authenticatorResponse ? ( + + + Obtained Device Response + + ) : ( + 'Authenticate Device' + )} + + + ) + } } export default U2FPrompt diff --git a/yarn.lock b/yarn.lock index 00f8d9c01bb..9aa85f9bbac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4780,7 +4780,7 @@ __metadata: resolution: "@standardnotes/api@workspace:packages/api" dependencies: "@standardnotes/common": ^1.46.6 - "@standardnotes/domain-core": ^1.11.3 + "@standardnotes/domain-core": ^1.12.0 "@standardnotes/encryption": "workspace:*" "@standardnotes/models": "workspace:*" "@standardnotes/responses": "workspace:*" @@ -5009,6 +5009,15 @@ __metadata: languageName: node linkType: hard +"@standardnotes/domain-core@npm:^1.12.0": + version: 1.12.0 + resolution: "@standardnotes/domain-core@npm:1.12.0" + dependencies: + uuid: ^9.0.0 + checksum: 852f15e481546fac621d503ea1cbdf9b2cb3e343d0c4cbe2fea7acc0f751a122d64479b55fa18e032e51d277a56621b9401d6f2d01756eb877eeb3572108e2a6 + languageName: node + linkType: hard + "@standardnotes/domain-events@npm:^2.108.1": version: 2.108.1 resolution: "@standardnotes/domain-events@npm:2.108.1" @@ -5086,7 +5095,7 @@ __metadata: resolution: "@standardnotes/features@workspace:packages/features" dependencies: "@standardnotes/common": ^1.46.6 - "@standardnotes/domain-core": ^1.11.3 + "@standardnotes/domain-core": ^1.12.0 "@standardnotes/security": ^1.7.6 "@types/jest": ^29.2.3 "@typescript-eslint/eslint-plugin": "*" @@ -5397,7 +5406,7 @@ __metadata: dependencies: "@standardnotes/api": "workspace:^" "@standardnotes/common": ^1.46.4 - "@standardnotes/domain-core": ^1.11.3 + "@standardnotes/domain-core": ^1.12.0 "@standardnotes/encryption": "workspace:^" "@standardnotes/files": "workspace:^" "@standardnotes/models": "workspace:^" @@ -5495,7 +5504,7 @@ __metadata: "@babel/preset-env": "*" "@standardnotes/api": "workspace:*" "@standardnotes/common": ^1.46.6 - "@standardnotes/domain-core": ^1.11.3 + "@standardnotes/domain-core": ^1.12.0 "@standardnotes/domain-events": ^2.108.1 "@standardnotes/encryption": "workspace:*" "@standardnotes/features": "workspace:*" From fb50131db6c26882d92cc2ab9b0a9e17b2676e56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Mon, 17 Apr 2023 14:04:29 +0200 Subject: [PATCH 2/2] chore: fix specs --- .../GetAuthenticatorAuthenticationResponse.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse.spec.ts b/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse.spec.ts index 37b08e4cfad..695171cd67e 100644 --- a/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse.spec.ts +++ b/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse.spec.ts @@ -12,7 +12,7 @@ describe('GetAuthenticatorAuthenticationResponse', () => { beforeEach(() => { getAuthenticatorAuthenticationOptions = {} as jest.Mocked - getAuthenticatorAuthenticationOptions.execute = jest.fn().mockResolvedValue({ foo: 'bar' }) + getAuthenticatorAuthenticationOptions.execute = jest.fn().mockResolvedValue(Result.ok({ foo: 'bar' })) authenticatorVerificationPromptFunction = jest.fn() })