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 000000000..7f3299643 Binary files /dev/null and b/.yarn/cache/@standardnotes-domain-core-npm-1.12.0-c4ef3cb073-852f15e481.zip differ diff --git a/packages/api/package.json b/packages/api/package.json index 58798903c..e0629c19d 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -37,7 +37,7 @@ }, "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:*", diff --git a/packages/features/package.json b/packages/features/package.json index acd792f67..f62711f9e 100644 --- a/packages/features/package.json +++ b/packages/features/package.json @@ -26,7 +26,7 @@ }, "dependencies": { "@standardnotes/common": "^1.46.6", - "@standardnotes/domain-core": "^1.11.3", + "@standardnotes/domain-core": "^1.12.0", "@standardnotes/security": "^1.7.6", "reflect-metadata": "^0.1.13" }, diff --git a/packages/mobile/android/app/build.gradle b/packages/mobile/android/app/build.gradle index e98d60d16..d37e75ae8 100644 --- a/packages/mobile/android/app/build.gradle +++ b/packages/mobile/android/app/build.gradle @@ -178,6 +178,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.1.0-rc01' implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0") implementation 'de.undercouch:gradle-download-task:5.0.2' + implementation 'com.google.android.gms:play-services-fido:20.0.1' debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") diff --git a/packages/mobile/android/app/src/main/java/com/standardnotes/Fido2ApiModule.java b/packages/mobile/android/app/src/main/java/com/standardnotes/Fido2ApiModule.java new file mode 100644 index 000000000..39ab58288 --- /dev/null +++ b/packages/mobile/android/app/src/main/java/com/standardnotes/Fido2ApiModule.java @@ -0,0 +1,203 @@ +package com.standardnotes; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Intent; +import android.content.IntentSender; +import android.util.Base64; +import android.util.Log; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ActivityEventListener; +import com.facebook.react.bridge.BaseActivityEventListener; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.WritableMap; +import java.util.Map; +import java.util.HashMap; +import java.util.ArrayList; + +import org.json.JSONObject; +import org.json.JSONArray; +import org.json.JSONException; + +import com.google.android.gms.fido.fido2.Fido2ApiClient; +import com.google.android.gms.fido.fido2.api.common.AuthenticatorErrorResponse; +import com.google.android.gms.fido.fido2.api.common.AuthenticatorAssertionResponse; +import com.google.android.gms.fido.fido2.api.common.AuthenticationExtensionsClientOutputs; +import com.google.android.gms.fido.fido2.api.common.PublicKeyCredential; +import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialType; +import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialDescriptor; +import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRequestOptions; +import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRequestOptions; +import com.google.android.gms.fido.Fido; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; +import com.google.android.gms.tasks.Task; + +public class Fido2ApiModule extends ReactContextBaseJavaModule { + private final Fido2ApiClient fido2ApiClient; + private final ReactApplicationContext reactContext; + private static final int SIGN_REQUEST_CODE = 111; + + private static final String LOGS_TAG = "Fido2ApiModule"; + private static final String RP_ID = "app.standardnotes.com"; + + private Promise signInPromise; + + private final ActivityEventListener activityEventListener = new BaseActivityEventListener() { + @Override + public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) { + super.onActivityResult(activity, requestCode, resultCode, intent); + + if (requestCode == SIGN_REQUEST_CODE) { + if (signInPromise != null) { + if (resultCode == Activity.RESULT_CANCELED) { + Log.e(LOGS_TAG, "FIDO sign in cancelled"); + + signInPromise.reject("FIDO sign in cancelled"); + } else if (resultCode == Activity.RESULT_OK) { + if (intent.hasExtra(Fido.FIDO2_KEY_ERROR_EXTRA)) { + AuthenticatorErrorResponse authenticatorErrorResponse = + AuthenticatorErrorResponse.deserializeFromBytes(intent.getByteArrayExtra(Fido.FIDO2_KEY_ERROR_EXTRA)); + Log.e(LOGS_TAG, "Fido Error: " + authenticatorErrorResponse.getErrorMessage()); + + signInPromise.reject(authenticatorErrorResponse.getErrorMessage()); + } else if (intent.hasExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA)) { + PublicKeyCredential publicKeyCredential = + PublicKeyCredential.deserializeFromBytes( + intent.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA)); + AuthenticatorAssertionResponse signedData = + (AuthenticatorAssertionResponse) publicKeyCredential.getResponse(); + + WritableMap signInResult = Arguments.createMap(); + signInResult.putString("id", Base64.encodeToString(signedData.getKeyHandle(), Base64.URL_SAFE)); + signInResult.putString("rawId", Base64.encodeToString(signedData.getKeyHandle(), Base64.URL_SAFE)); + + byte[] extensionOutputsBytes = null; + AuthenticationExtensionsClientOutputs extensionOutputs = publicKeyCredential.getClientExtensionResults(); + if (extensionOutputs != null) { + extensionOutputsBytes = extensionOutputs.serializeToBytes(); + if (extensionOutputsBytes != null) { + signInResult.putString("clientExtensionResults", Base64.encodeToString(extensionOutputsBytes, Base64.URL_SAFE)); + } + } + + WritableMap response = Arguments.createMap(); + response.putString("clientDataJSON", Base64.encodeToString(signedData.getClientDataJSON(), Base64.URL_SAFE)); + response.putString("authenticatorData", Base64.encodeToString(signedData.getAuthenticatorData(), Base64.URL_SAFE)); + response.putString("signature", Base64.encodeToString(signedData.getSignature(), Base64.URL_SAFE)); + byte[] userHandle = signedData.getUserHandle(); + if (userHandle != null) { + response.putString("userHandle", Base64.encodeToString(userHandle, Base64.URL_SAFE)); + } + signInResult.putMap("response", response); + + signInPromise.resolve(signInResult); + } + } + } + signInPromise = null; + } + } + }; + + Fido2ApiModule(ReactApplicationContext context) { + super(context); + + fido2ApiClient = Fido.getFido2ApiClient(context); + context.addActivityEventListener(activityEventListener); + + this.reactContext = context; + } + + @Override + public String getName() { + return "Fido2ApiModule"; + } + + @ReactMethod + public void promptForU2FAuthentication(String authenticationOptionsJSONString, Promise promise) throws JSONException { + signInPromise = promise; + + JSONObject authenticationOptions = new JSONObject(authenticationOptionsJSONString); + + ArrayList 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 000000000..fdc54a46b --- /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 11da2f3dc..fd6570cc7 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 @@ public class MainApplication extends Application implements ReactApplication { @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 bd233cc38..2642fcf3b 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 37ed3e77f..da473e474 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 15a445c5e..52ab46bd2 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 d46efc75e..022435cdf 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 a437fcac3..4f0488956 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 000000000..8c1e1a177 --- /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 000000000..7c4210665 --- /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 000000000..55886823b --- /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 882e7a9b9..695171cd6 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(Result.ok({ 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 fad5fe6e1..6f3a102be 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 06c72a5a4..25cda4e27 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 d1fe689aa..46d068f28 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 66bd5943f..e3ef80ae2 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 7923e6d84..632b9c4df 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 c62bd31c0..c74c9ee71 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}
} + +
+ ) } - - return ( -
- {error &&
{error}
} - -
- ) } export default U2FPrompt diff --git a/yarn.lock b/yarn.lock index 00f8d9c01..9aa85f9bb 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:*"