Skip to content

Commit 1b0055b

Browse files
committed
ios/android: fix screen lock bugs
On some Android versions (e.g. Android 9) the authentication flow was causing an endless loop of auth requests. This was fixed by moving the setup of `BiometricAuthHelper` from `onStart` to `onCreate`. In both Android and iOS the auth flow freezed if there was no authentication method setup on the device. This was fixed by refactoring the authentication flow code and adding a new possible authentication response `authres-missing`, that allows to handle this case.
1 parent c9a9010 commit 1b0055b

File tree

14 files changed

+183
-120
lines changed

14 files changed

+183
-120
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
- Fix wrong btc/ltc transaction timestamp during header sync
1111
- Ethereum bugfix: show all internal transactions that share the same transaction ID
1212
- Allow up to 6 unused BTC/LTC accounts (previously 5)
13+
- Android: fix screen lock authentication loop bug
14+
- Android/iOS: fix screen lock bug when no authentication is configured on the device
1315

1416
## v4.48.4
1517
- macOS: fix potential USB communication issue with BitBox02 bootloaders <v1.1.2 and firmwares <v9.23.1

backend/backend.go

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -127,16 +127,35 @@ type deviceEvent struct {
127127

128128
type authEventType string
129129

130+
// AuthResultType represents the possible results of the authentication flow.
131+
type AuthResultType string
132+
130133
const (
134+
// authRequired is fired when we need the user to authenticate.
131135
authRequired authEventType = "auth-required"
132-
authForced authEventType = "auth-forced"
133-
authCanceled authEventType = "auth-canceled"
134-
authOk authEventType = "auth-ok"
135-
authErr authEventType = "auth-err"
136+
// authForced is fired when we need the user to authenticate even if
137+
// the authentication flag is not set in the settings. This allows to check
138+
// that the user is able to authenticate before enabling the screen lock.
139+
authForced authEventType = "auth-forced"
140+
// authResult is fired when the authentication flow produced a result.
141+
authResult authEventType = "auth-result"
142+
143+
// AuthResultOk means that the authentication succeeded.
144+
AuthResultOk AuthResultType = "authres-ok"
145+
// AuthResultErr means that there is an authentication error.
146+
// E.g. on Android when a biometric is valid, but not recognized.
147+
AuthResultErr AuthResultType = "authres-err"
148+
// AuthResultCancel menas that the authentication has been aborted
149+
// by the user.
150+
AuthResultCancel AuthResultType = "authres-cancel"
151+
// AuthResultMissing means that there is no authentication method configured
152+
// on the device.
153+
AuthResultMissing AuthResultType = "authres-missing"
136154
)
137155

138156
type authEventObject struct {
139-
Typ authEventType `json:"typ"`
157+
Typ authEventType `json:"typ"`
158+
Result *AuthResultType `json:"result,omitempty"`
140159
}
141160

142161
// Environment represents functionality where the implementation depends on the environment the app
@@ -373,7 +392,7 @@ func (backend *Backend) Authenticate(force bool) {
373392
if backend.config.AppConfig().Backend.Authentication || force {
374393
backend.environment.Auth()
375394
} else {
376-
backend.AuthResult(true)
395+
backend.AuthResult(AuthResultOk)
377396
}
378397
}
379398

@@ -388,17 +407,6 @@ func (backend *Backend) TriggerAuth() {
388407
})
389408
}
390409

391-
// CancelAuth triggers an auth-canceled notification.
392-
func (backend *Backend) CancelAuth() {
393-
backend.Notify(observable.Event{
394-
Subject: "auth",
395-
Action: action.Replace,
396-
Object: authEventObject{
397-
Typ: authCanceled,
398-
},
399-
})
400-
}
401-
402410
// ForceAuth triggers an auth-forced notification
403411
// followed by an auth-required notification.
404412
func (backend *Backend) ForceAuth() {
@@ -420,17 +428,14 @@ func (backend *Backend) ForceAuth() {
420428

421429
// AuthResult triggers an auth-ok or auth-err notification
422430
// depending on the input value.
423-
func (backend *Backend) AuthResult(ok bool) {
424-
backend.log.Infof("Auth result: %v", ok)
425-
typ := authErr
426-
if ok {
427-
typ = authOk
428-
}
431+
func (backend *Backend) AuthResult(result AuthResultType) {
432+
backend.log.Infof("Auth result: %v", result)
429433
backend.Notify(observable.Event{
430434
Subject: "auth",
431435
Action: action.Replace,
432436
Object: authEventObject{
433-
Typ: typ,
437+
Typ: authResult,
438+
Result: &result,
434439
},
435440
})
436441
}

backend/bridgecommon/bridgecommon.go

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -143,25 +143,16 @@ func TriggerAuth() {
143143
globalBackend.TriggerAuth()
144144
}
145145

146-
// CancelAuth triggers an authentication canceled notification.
147-
func CancelAuth() {
148-
mu.Lock()
149-
defer mu.Unlock()
150-
if globalBackend == nil {
151-
return
152-
}
153-
globalBackend.CancelAuth()
154-
}
155-
156146
// AuthResult triggers an authentication result notification
157147
// on the base of the input value.
158-
func AuthResult(ok bool) {
148+
func AuthResult(result string) {
159149
mu.Lock()
160150
defer mu.Unlock()
161151
if globalBackend == nil {
162152
return
163153
}
164-
globalBackend.AuthResult(ok)
154+
155+
globalBackend.AuthResult(backend.AuthResultType(result))
165156
}
166157

167158
// UsingMobileDataChanged should be called when the network connnection changed.

backend/mobileserver/mobileserver.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"sync"
2424
"time"
2525

26+
"github.com/BitBoxSwiss/bitbox-wallet-app/backend"
2627
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/bridgecommon"
2728
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/usb"
2829
"github.com/BitBoxSwiss/bitbox-wallet-app/util/config"
@@ -34,6 +35,17 @@ var (
3435
once sync.Once
3536
)
3637

38+
const (
39+
// AuthResultOk exports backend.AuthResultOk.
40+
AuthResultOk string = string(backend.AuthResultOk)
41+
// AuthResultErr exports backend.AuthResultErr.
42+
AuthResultErr string = string(backend.AuthResultErr)
43+
// AuthResultCancel exports backend.AuthResultCancel.
44+
AuthResultCancel string = string(backend.AuthResultCancel)
45+
// AuthResultMissing exports backend.AuthResultMissing.
46+
AuthResultMissing string = string(backend.AuthResultMissing)
47+
)
48+
3749
// fixTimezone sets the local timezone on Android. This is a workaround to the bug that on Android,
3850
// time.Local is hard-coded to UTC. See https://github.com/golang/go/issues/20455.
3951
//
@@ -235,15 +247,10 @@ func TriggerAuth() {
235247
bridgecommon.TriggerAuth()
236248
}
237249

238-
// CancelAuth triggers an auth canceled notification towards the frontend.
239-
func CancelAuth() {
240-
bridgecommon.CancelAuth()
241-
}
242-
243-
// AuthResult triggers an auth feedback notification (auth-ok/auth-err) towards the frontend,
250+
// AuthResult triggers an auth feedback notification (auth-ok/auth-err/..) towards the frontend,
244251
// depending on the input value.
245-
func AuthResult(ok bool) {
246-
bridgecommon.AuthResult(ok)
252+
func AuthResult(result string) {
253+
bridgecommon.AuthResult(result)
247254
}
248255

249256
// ManualReconnect wraps bridgecommon.ManualReconnect.

cmd/servewallet/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func (webdevEnvironment) Auth() {
8787
log := logging.Get().WithGroup("servewallet")
8888
log.Info("Webdev Auth")
8989
if backend != nil {
90-
backend.AuthResult(true)
90+
backend.AuthResult(backendPkg.AuthResultOk)
9191
log.Info("Webdev Auth OK")
9292
}
9393
}

frontends/android/BitBoxApp/app/src/main/java/ch/shiftcrypto/bitboxapp/BiometricAuthHelper.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,23 @@ public interface AuthCallback {
1717
void onSuccess();
1818
void onFailure();
1919
void onCancel();
20+
void noAuthConfigured();
2021
}
2122

2223
public static void showAuthenticationPrompt(FragmentActivity activity, AuthCallback callback) {
24+
25+
BiometricManager biometricManager = BiometricManager.from(activity);
26+
int canAuthenticate = biometricManager.canAuthenticate(
27+
BiometricManager.Authenticators.DEVICE_CREDENTIAL |
28+
BiometricManager.Authenticators.BIOMETRIC_WEAK
29+
);
30+
31+
if (canAuthenticate != BiometricManager.BIOMETRIC_SUCCESS) {
32+
Util.log("Authentication not available: code " + canAuthenticate);
33+
new Handler(Looper.getMainLooper()).post(callback::noAuthConfigured);
34+
return;
35+
}
36+
2337
Executor executor = ContextCompat.getMainExecutor(activity);
2438
BiometricPrompt biometricPrompt = new BiometricPrompt(activity, executor, new BiometricPrompt.AuthenticationCallback() {
2539
@Override

frontends/android/BitBoxApp/app/src/main/java/ch/shiftcrypto/bitboxapp/GoViewModel.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ public boolean detectDarkTheme() {
202202
}
203203

204204
private final MutableLiveData<Boolean> isDarkTheme = new MutableLiveData<>();
205-
private final MutableLiveData<Boolean> authenticator = new MutableLiveData<>(false);
205+
private final MutableLiveData<Boolean> authRequested = new MutableLiveData<>(false);
206206
// The value of the backend config's Authentication setting.
207207
private final MutableLiveData<Boolean> authSetting = new MutableLiveData<>(false);
208208
private final GoEnvironment goEnvironment;
@@ -218,8 +218,8 @@ public MutableLiveData<Boolean> getIsDarkTheme() {
218218
return isDarkTheme;
219219
}
220220

221-
public MutableLiveData<Boolean> getAuthenticator() {
222-
return authenticator;
221+
public MutableLiveData<Boolean> getAuthRequested() {
222+
return authRequested;
223223
}
224224

225225
public MutableLiveData<Boolean> getAuthSetting() {
@@ -235,11 +235,11 @@ public GoAPI getGoAPI() {
235235
}
236236

237237
public void requestAuth() {
238-
this.authenticator.postValue(true);
238+
this.authRequested.postValue(true);
239239
}
240240

241241
public void closeAuth() {
242-
this.authenticator.postValue(false);
242+
this.authRequested.postValue(false);
243243
}
244244

245245
public void setMessageHandlers(Handler callResponseHandler, Handler pushNotificationHandler) {

frontends/android/BitBoxApp/app/src/main/java/ch/shiftcrypto/bitboxapp/MainActivity.java

Lines changed: 48 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -269,42 +269,9 @@ public void handleOnBackPressed() {
269269
backPressedHandler();
270270
}
271271
});
272-
}
273-
274-
@Override
275-
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
276-
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
277-
if (requestCode == PERMISSIONS_REQUEST_CAMERA_QRCODE) {
278-
webChrome.onCameraPermissionResult(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED);
279-
}
280-
}
281-
282-
private void startServer() {
283-
final GoViewModel gVM = ViewModelProviders.of(this).get(GoViewModel.class);
284-
goService.startServer(getApplicationContext().getFilesDir().getAbsolutePath(), gVM.getGoEnvironment(), gVM.getGoAPI());
285-
286-
// Trigger connectivity check (as the network may already be unavailable when the app starts).
287-
checkConnectivity();
288-
}
289-
290-
@Override
291-
protected void onNewIntent(Intent intent) {
292-
// This is only called reliably when intents are received (e.g. USB is attached or when
293-
// handling 'aopp:' URIs through the android.intent.action.VIEW intent) with
294-
// android:launchMode="singleTop"
295-
super.onNewIntent(intent);
296-
setIntent(intent); // make sure onResume will have access to this intent
297-
}
298-
299-
@Override
300-
protected void onStart() {
301-
super.onStart();
302-
Util.log("lifecycle: onStart");
303-
final GoViewModel goViewModel = ViewModelProviders.of(this).get(GoViewModel.class);
304-
goViewModel.getIsDarkTheme().observe(this, this::setDarkTheme);
305272

306-
goViewModel.getAuthenticator().observe(this, requestAuth -> {
307-
if (!requestAuth) {
273+
goViewModel.getAuthRequested().observe(this, authRequested -> {
274+
if (!authRequested) {
308275
return;
309276
}
310277

@@ -314,23 +281,31 @@ public void onSuccess() {
314281
// Authenticated successfully
315282
Util.log("Auth success");
316283
goViewModel.closeAuth();
317-
Mobileserver.authResult(true);
284+
Mobileserver.authResult(Mobileserver.AuthResultOk);
318285
}
319286

320287
@Override
321288
public void onFailure() {
322289
// Failed
323290
Util.log("Auth failed");
324291
goViewModel.closeAuth();
325-
Mobileserver.authResult(false);
292+
Mobileserver.authResult(Mobileserver.AuthResultErr);
326293
}
327294

328295
@Override
329296
public void onCancel() {
330297
// Canceled
331298
Util.log("Auth canceled");
332299
goViewModel.closeAuth();
333-
Mobileserver.cancelAuth();
300+
Mobileserver.authResult(Mobileserver.AuthResultCancel);
301+
}
302+
303+
@Override
304+
public void noAuthConfigured() {
305+
// No Auth configured
306+
Util.log("Auth not configured");
307+
goViewModel.closeAuth();
308+
Mobileserver.authResult(Mobileserver.AuthResultMissing);
334309
}
335310
});
336311
});
@@ -347,6 +322,41 @@ public void onCancel() {
347322
}
348323
}));
349324

325+
}
326+
327+
@Override
328+
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
329+
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
330+
if (requestCode == PERMISSIONS_REQUEST_CAMERA_QRCODE) {
331+
webChrome.onCameraPermissionResult(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED);
332+
}
333+
}
334+
335+
private void startServer() {
336+
final GoViewModel gVM = ViewModelProviders.of(this).get(GoViewModel.class);
337+
goService.startServer(getApplicationContext().getFilesDir().getAbsolutePath(), gVM.getGoEnvironment(), gVM.getGoAPI());
338+
339+
// Trigger connectivity check (as the network may already be unavailable when the app starts).
340+
checkConnectivity();
341+
}
342+
343+
@Override
344+
protected void onNewIntent(Intent intent) {
345+
// This is only called reliably when intents are received (e.g. USB is attached or when
346+
// handling 'aopp:' URIs through the android.intent.action.VIEW intent) with
347+
// android:launchMode="singleTop"
348+
super.onNewIntent(intent);
349+
setIntent(intent); // make sure onResume will have access to this intent
350+
}
351+
352+
@Override
353+
protected void onStart() {
354+
super.onStart();
355+
Util.log("lifecycle: onStart");
356+
final GoViewModel goViewModel = ViewModelProviders.of(this).get(GoViewModel.class);
357+
goViewModel.getIsDarkTheme().observe(this, this::setDarkTheme);
358+
359+
350360
NetworkRequest request = new NetworkRequest.Builder()
351361
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
352362
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)

frontends/ios/BitBoxApp/BitBoxApp/BitBoxAppApp.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,21 +43,21 @@ class GoEnvironment: NSObject, MobileserverGoEnvironmentInterfaceProtocol, UIDoc
4343
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, authenticationError in
4444
DispatchQueue.main.async {
4545
if success {
46-
MobileserverAuthResult(true);
46+
MobileserverAuthResult(MobileserverAuthResultOk);
4747
} else {
4848
if let laError = authenticationError as? LAError,
4949
laError.code == .userCancel {
50-
MobileserverCancelAuth();
50+
MobileserverAuthResult(MobileserverAuthResultCancel);
5151
} else {
52-
MobileserverAuthResult(false);
52+
MobileserverAuthResult(MobileserverAuthResultErr);
5353
}
5454
}
5555
}
5656
}
5757
} else {
5858
// Biometric authentication not available
5959
DispatchQueue.main.async {
60-
MobileserverAuthResult(false);
60+
MobileserverAuthResult(MobileserverAuthResultMissing);
6161
}
6262
}
6363
}

0 commit comments

Comments
 (0)