Skip to content

Commit a955f53

Browse files
benmaBeerosagos
authored andcommitted
frontends/android: add an authentication feature
Watch-only can benefit from a feature that allows the user to authenticate when opening the bitboxapp, to protect the privacy. This commit creates an Auth component in the frontend that can trigger an environment-based authentication. On Qt and Webdev the authentication succeds automatically, while on Android it requires the user to complete the system biometric authentication every time the app is resumed (.i.e. at startup and when the app comes to foreground after being put in the background). The authentication can be requested by the frontend using the 'auth' endpoint. Authentication messages from backend to frontend are sent via a new 'auth' notifications, which contains a Typ string: - 'auth-required': which triggers a new `auth` call from the frontend - 'auth-ok': which means authentication completed successfully - 'auth-err': which means authentication failed or has been cancelled by the user.
1 parent 888f481 commit a955f53

File tree

18 files changed

+314
-6
lines changed

18 files changed

+314
-6
lines changed

backend/accounts_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,8 @@ func (e environment) DetectDarkTheme() bool {
310310
return false
311311
}
312312

313+
func (e environment) Auth() {}
314+
313315
func newBackend(t *testing.T, testing, regtest bool) *Backend {
314316
t.Helper()
315317
b, err := NewBackend(

backend/backend.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,18 @@ type AccountEvent struct {
112112
Data string `json:"data"`
113113
}
114114

115+
type authEventType string
116+
117+
const (
118+
authRequired authEventType = "auth-required"
119+
authOk authEventType = "auth-ok"
120+
authErr authEventType = "auth-err"
121+
)
122+
123+
type authEventObject struct {
124+
Typ authEventType `json:"typ"`
125+
}
126+
115127
// Environment represents functionality where the implementation depends on the environment the app
116128
// runs in, e.g. Qt5/Mobile/webdev.
117129
type Environment interface {
@@ -143,6 +155,7 @@ type Environment interface {
143155
SetDarkTheme(bool)
144156
// DetectDarkTheme returns true if the dark theme is enabled at OS level.
145157
DetectDarkTheme() bool
158+
Auth()
146159
}
147160

148161
// Backend ties everything together and is the main starting point to use the BitBox wallet library.
@@ -296,6 +309,31 @@ func (backend *Backend) Config() *config.Config {
296309
return backend.config
297310
}
298311

312+
func (backend *Backend) TriggerAuth() {
313+
backend.Notify(observable.Event{
314+
Subject: "auth",
315+
Action: action.Replace,
316+
Object: authEventObject{
317+
Typ: authRequired,
318+
},
319+
})
320+
}
321+
322+
func (backend *Backend) AuthResult(ok bool) {
323+
backend.log.Infof("Auth result: %v", ok)
324+
typ := authErr
325+
if ok {
326+
typ = authOk
327+
}
328+
backend.Notify(observable.Event{
329+
Subject: "auth",
330+
Action: action.Replace,
331+
Object: authEventObject{
332+
Typ: typ,
333+
},
334+
})
335+
}
336+
299337
// DefaultAppConfig returns the default app config.
300338
func (backend *Backend) DefaultAppConfig() config.AppConfig {
301339
return config.NewDefaultAppConfig()

backend/bridgecommon/bridgecommon.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,27 @@ func HandleURI(uri string) {
121121
globalBackend.HandleURI(uri)
122122
}
123123

124+
// TriggerAuth triggers an authentication request notification.
125+
func TriggerAuth() {
126+
mu.Lock()
127+
defer mu.Unlock()
128+
if globalBackend == nil {
129+
return
130+
}
131+
globalBackend.TriggerAuth()
132+
}
133+
134+
// AuthResult triggers an authentication result notification
135+
// on the base of the input value.
136+
func AuthResult(ok bool) {
137+
mu.Lock()
138+
defer mu.Unlock()
139+
if globalBackend == nil {
140+
return
141+
}
142+
globalBackend.AuthResult(ok)
143+
}
144+
124145
// UsingMobileDataChanged should be called when the network connnection changed.
125146
func UsingMobileDataChanged() {
126147
mu.RLock()
@@ -147,6 +168,7 @@ type BackendEnvironment struct {
147168
GetSaveFilenameFunc func(string) string
148169
SetDarkThemeFunc func(bool)
149170
DetectDarkThemeFunc func() bool
171+
AuthFunc func()
150172
}
151173

152174
// NotifyUser implements backend.Environment.
@@ -156,6 +178,13 @@ func (env *BackendEnvironment) NotifyUser(text string) {
156178
}
157179
}
158180

181+
// Auth implements backend.Environment.
182+
func (env *BackendEnvironment) Auth() {
183+
if env.AuthFunc != nil {
184+
env.AuthFunc()
185+
}
186+
}
187+
159188
// DeviceInfos implements backend.Environment.
160189
func (env *BackendEnvironment) DeviceInfos() []usb.DeviceInfo {
161190
if env.DeviceInfosFunc != nil {

backend/bridgecommon/bridgecommon_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ func (e environment) DetectDarkTheme() bool {
6969
return false
7070
}
7171

72+
func (e environment) Auth() {}
73+
7274
// TestServeShutdownServe checks that you can call Serve twice in a row.
7375
func TestServeShutdownServe(t *testing.T) {
7476
bridgecommon.Serve(

backend/handlers/handlers.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ func NewHandlers(
194194
getAPIRouterNoError(apiRouter)("/update", handlers.getUpdateHandler).Methods("GET")
195195
getAPIRouterNoError(apiRouter)("/banners/{key}", handlers.getBannersHandler).Methods("GET")
196196
getAPIRouterNoError(apiRouter)("/using-mobile-data", handlers.getUsingMobileDataHandler).Methods("GET")
197+
getAPIRouterNoError(apiRouter)("/auth", handlers.getAuthHandler).Methods("GET")
197198
getAPIRouter(apiRouter)("/set-dark-theme", handlers.postDarkThemeHandler).Methods("POST")
198199
getAPIRouterNoError(apiRouter)("/detect-dark-theme", handlers.getDetectDarkThemeHandler).Methods("GET")
199200
getAPIRouterNoError(apiRouter)("/version", handlers.getVersionHandler).Methods("GET")
@@ -459,6 +460,12 @@ func (handlers *Handlers) getUsingMobileDataHandler(r *http.Request) interface{}
459460
return handlers.backend.Environment().UsingMobileData()
460461
}
461462

463+
func (handlers *Handlers) getAuthHandler(r *http.Request) interface{} {
464+
handlers.log.Info("Auth requested")
465+
handlers.backend.Environment().Auth()
466+
return nil
467+
}
468+
462469
func (handlers *Handlers) postDarkThemeHandler(r *http.Request) (interface{}, error) {
463470
var isDark bool
464471
if err := json.NewDecoder(r.Body).Decode(&isDark); err != nil {

backend/handlers/handlers_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ func (e *backendEnv) NativeLocale() string { return e.Locale }
5858
func (e *backendEnv) GetSaveFilename(string) string { return "" }
5959
func (e *backendEnv) SetDarkTheme(bool) {}
6060
func (e *backendEnv) DetectDarkTheme() bool { return false }
61+
func (e *backendEnv) Auth() {}
6162

6263
func TestGetNativeLocale(t *testing.T) {
6364
const ptLocale = "pt"

cmd/servewallet/main.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
"runtime"
2424
"strings"
2525

26-
"github.com/digitalbitbox/bitbox-wallet-app/backend"
26+
backendPkg "github.com/digitalbitbox/bitbox-wallet-app/backend"
2727
"github.com/digitalbitbox/bitbox-wallet-app/backend/arguments"
2828
btctypes "github.com/digitalbitbox/bitbox-wallet-app/backend/coins/btc/types"
2929
"github.com/digitalbitbox/bitbox-wallet-app/backend/devices/usb"
@@ -39,6 +39,8 @@ const (
3939
address = "0.0.0.0"
4040
)
4141

42+
var backend *backendPkg.Backend
43+
4244
// webdevEnvironment implements backend.Environment.
4345
type webdevEnvironment struct {
4446
}
@@ -80,6 +82,16 @@ func (webdevEnvironment) UsingMobileData() bool {
8082
return false
8183
}
8284

85+
// Auth implements backend.Environment.
86+
func (webdevEnvironment) Auth() {
87+
log := logging.Get().WithGroup("servewallet")
88+
log.Info("Webdev Auth")
89+
if backend != nil {
90+
backend.AuthResult(true)
91+
log.Info("Webdev Auth OK")
92+
}
93+
}
94+
8395
// NativeLocale naively implements backend.Environment.
8496
// This version is unlikely to work on Windows.
8597
func (webdevEnvironment) NativeLocale() string {
@@ -141,7 +153,7 @@ func main() {
141153
log.Info("--------------- Started application --------------")
142154
// since we are in dev-mode, we can drop the authorization token
143155
connectionData := backendHandlers.NewConnectionData(-1, "")
144-
backend, err := backend.NewBackend(
156+
newBackend, err := backendPkg.NewBackend(
145157
arguments.NewArguments(
146158
config.AppDir(),
147159
!*mainnet,
@@ -153,6 +165,7 @@ func main() {
153165
if err != nil {
154166
log.WithField("error", err).Panic(err)
155167
}
168+
backend = newBackend
156169
handlers := backendHandlers.NewHandlers(backend, connectionData)
157170
log.WithFields(logrus.Fields{"address": address, "port": port}).Info("Listening for HTTP")
158171
fmt.Printf("Listening on: http://localhost:%d\n", port)

frontends/android/BitBoxApp/app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ android {
2828
dependencies {
2929
implementation fileTree(dir: 'libs', include: ['*.jar'])
3030
implementation 'androidx.appcompat:appcompat:1.0.2'
31+
implementation 'androidx.biometric:biometric:1.1.0'
3132
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
3233
testImplementation 'junit:junit:4.12'
3334
androidTestImplementation 'androidx.test:runner:1.2.0'
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package ch.shiftcrypto.bitboxapp;
2+
3+
import android.os.Handler;
4+
import android.os.Looper;
5+
6+
import androidx.annotation.NonNull;
7+
import androidx.biometric.BiometricManager;
8+
import androidx.biometric.BiometricPrompt;
9+
import androidx.core.content.ContextCompat;
10+
import androidx.fragment.app.FragmentActivity;
11+
12+
import java.util.concurrent.Executor;
13+
14+
public class BiometricAuthHelper {
15+
16+
public interface AuthCallback {
17+
void onSuccess();
18+
void onFailure();
19+
}
20+
21+
public static void showAuthenticationPrompt(FragmentActivity activity, AuthCallback callback) {
22+
Executor executor = ContextCompat.getMainExecutor(activity);
23+
BiometricPrompt biometricPrompt = new BiometricPrompt(activity, executor, new BiometricPrompt.AuthenticationCallback() {
24+
@Override
25+
public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
26+
super.onAuthenticationSucceeded(result);
27+
new Handler(Looper.getMainLooper()).post(callback::onSuccess);
28+
}
29+
30+
@Override
31+
public void onAuthenticationFailed() {
32+
super.onAuthenticationFailed();
33+
new Handler(Looper.getMainLooper()).post(callback::onFailure);
34+
}
35+
36+
@Override
37+
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
38+
Util.log("Authentication error: " + errorCode + " - " + errString);
39+
super.onAuthenticationError(errorCode, errString);
40+
new Handler(Looper.getMainLooper()).post(callback::onFailure);
41+
}
42+
});
43+
44+
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
45+
.setTitle("Authentication required")
46+
.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL |
47+
BiometricManager.Authenticators.BIOMETRIC_WEAK)
48+
.setConfirmationRequired(false)
49+
.build();
50+
51+
biometricPrompt.authenticate(promptInfo);
52+
}
53+
}

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ public void systemOpen(String url) throws Exception {
121121
Util.systemOpen(getApplication(), url);
122122
}
123123

124+
public void auth() {
125+
Util.log("Auth requested from backend");
126+
requestAuth();
127+
}
128+
124129
public boolean usingMobileData() {
125130
// Adapted from https://stackoverflow.com/a/53243938
126131

@@ -187,15 +192,29 @@ public void pushNotify(String msg) {
187192
}
188193
}
189194

190-
private MutableLiveData<Boolean> isDarkTheme = new MutableLiveData<>();
195+
private MutableLiveData<Boolean> isDarkTheme = new MutableLiveData<>();
191196

192-
public MutableLiveData<Boolean> getIsDarkTheme() {
197+
public MutableLiveData<Boolean> getIsDarkTheme() {
193198
return isDarkTheme;
194199
}
195-
196-
public void setIsDarkTheme(Boolean isDark) {
200+
public void setIsDarkTheme(Boolean isDark) {
197201
this.isDarkTheme.postValue(isDark);
198202
}
203+
204+
private MutableLiveData<Boolean> authenticator = new MutableLiveData<>(false);
205+
206+
public MutableLiveData<Boolean> getAuthenticator() {
207+
return authenticator;
208+
}
209+
210+
public void requestAuth() {
211+
this.authenticator.postValue(true);
212+
}
213+
214+
public void closeAuth() {
215+
this.authenticator.postValue(false);
216+
}
217+
199218
private GoEnvironment goEnvironment;
200219
private GoAPI goAPI;
201220

0 commit comments

Comments
 (0)