Skip to content

Commit e2ea023

Browse files
committed
Merge branch 'android-auth' into staging-watchonly
2 parents 604c904 + 95b527a commit e2ea023

File tree

22 files changed

+524
-7
lines changed

22 files changed

+524
-7
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: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,20 @@ type AccountEvent struct {
112112
Data string `json:"data"`
113113
}
114114

115+
type authEventType string
116+
117+
const (
118+
authRequired authEventType = "auth-required"
119+
authForced authEventType = "auth-forced"
120+
authCanceled authEventType = "auth-canceled"
121+
authOk authEventType = "auth-ok"
122+
authErr authEventType = "auth-err"
123+
)
124+
125+
type authEventObject struct {
126+
Typ authEventType `json:"typ"`
127+
}
128+
115129
// Environment represents functionality where the implementation depends on the environment the app
116130
// runs in, e.g. Qt5/Mobile/webdev.
117131
type Environment interface {
@@ -143,6 +157,7 @@ type Environment interface {
143157
SetDarkTheme(bool)
144158
// DetectDarkTheme returns true if the dark theme is enabled at OS level.
145159
DetectDarkTheme() bool
160+
Auth()
146161
}
147162

148163
// Backend ties everything together and is the main starting point to use the BitBox wallet library.
@@ -296,6 +311,79 @@ func (backend *Backend) Config() *config.Config {
296311
return backend.config
297312
}
298313

314+
// Authenticate executes a system authentication if
315+
// the authentication config flag is enabled or if the
316+
// `force` input flag is enabled (as a consequence of an
317+
// 'auth/auth-forced' notification).
318+
// Otherwise, the authentication is automatically assumed as
319+
// successful.
320+
func (backend *Backend) Authenticate(force bool) {
321+
backend.log.Info("Auth requested")
322+
if backend.config.AppConfig().Backend.Authentication || force {
323+
backend.environment.Auth()
324+
} else {
325+
backend.AuthResult(true)
326+
}
327+
}
328+
329+
// TriggerAuth triggers an auth-required notification.
330+
func (backend *Backend) TriggerAuth() {
331+
backend.Notify(observable.Event{
332+
Subject: "auth",
333+
Action: action.Replace,
334+
Object: authEventObject{
335+
Typ: authRequired,
336+
},
337+
})
338+
}
339+
340+
// CancelAuth triggers an auth-canceled notification.
341+
func (backend *Backend) CancelAuth() {
342+
backend.Notify(observable.Event{
343+
Subject: "auth",
344+
Action: action.Replace,
345+
Object: authEventObject{
346+
Typ: authCanceled,
347+
},
348+
})
349+
}
350+
351+
// ForceAuth triggers an auth-forced notification
352+
// followed by an auth-required notification.
353+
func (backend *Backend) ForceAuth() {
354+
backend.Notify(observable.Event{
355+
Subject: "auth",
356+
Action: action.Replace,
357+
Object: authEventObject{
358+
Typ: authForced,
359+
},
360+
})
361+
backend.Notify(observable.Event{
362+
Subject: "auth",
363+
Action: action.Replace,
364+
Object: authEventObject{
365+
Typ: authRequired,
366+
},
367+
})
368+
}
369+
370+
// AuthResult triggers an auth-ok or auth-err notification
371+
// depending on the input value.
372+
func (backend *Backend) AuthResult(ok bool) {
373+
backend.log.Infof("Auth result: %v", ok)
374+
typ := authErr
375+
if ok {
376+
typ = authOk
377+
}
378+
backend.Notify(observable.Event{
379+
Subject: "auth",
380+
Action: action.Replace,
381+
Object: authEventObject{
382+
Typ: typ,
383+
},
384+
})
385+
}
386+
299387
// DefaultAppConfig returns the default app config.
300388
func (backend *Backend) DefaultAppConfig() config.AppConfig {
301389
return config.NewDefaultAppConfig()

backend/bridgecommon/bridgecommon.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,37 @@ 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+
// CancelAuth triggers an authentication canceled notification.
135+
func CancelAuth() {
136+
mu.Lock()
137+
defer mu.Unlock()
138+
if globalBackend == nil {
139+
return
140+
}
141+
globalBackend.CancelAuth()
142+
}
143+
144+
// AuthResult triggers an authentication result notification
145+
// on the base of the input value.
146+
func AuthResult(ok bool) {
147+
mu.Lock()
148+
defer mu.Unlock()
149+
if globalBackend == nil {
150+
return
151+
}
152+
globalBackend.AuthResult(ok)
153+
}
154+
124155
// UsingMobileDataChanged should be called when the network connnection changed.
125156
func UsingMobileDataChanged() {
126157
mu.RLock()
@@ -147,6 +178,7 @@ type BackendEnvironment struct {
147178
GetSaveFilenameFunc func(string) string
148179
SetDarkThemeFunc func(bool)
149180
DetectDarkThemeFunc func() bool
181+
AuthFunc func()
150182
}
151183

152184
// NotifyUser implements backend.Environment.
@@ -156,6 +188,13 @@ func (env *BackendEnvironment) NotifyUser(text string) {
156188
}
157189
}
158190

191+
// Auth implements backend.Environment.
192+
func (env *BackendEnvironment) Auth() {
193+
if env.AuthFunc != nil {
194+
env.AuthFunc()
195+
}
196+
}
197+
159198
// DeviceInfos implements backend.Environment.
160199
func (env *BackendEnvironment) DeviceInfos() []usb.DeviceInfo {
161200
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/config/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ type Backend struct {
7474
DeprecatedLitecoinActive bool `json:"litecoinActive"`
7575
DeprecatedEthereumActive bool `json:"ethereumActive"`
7676

77+
Authentication bool `json:"authentication"`
78+
7779
BTC btcCoinConfig `json:"btc"`
7880
TBTC btcCoinConfig `json:"tbtc"`
7981
RBTC btcCoinConfig `json:"rbtc"`
@@ -155,6 +157,7 @@ func NewDefaultAppConfig() AppConfig {
155157
UseProxy: false,
156158
ProxyAddress: "",
157159
},
160+
Authentication: false,
158161
DeprecatedBitcoinActive: true,
159162
DeprecatedLitecoinActive: true,
160163
DeprecatedEthereumActive: true,

backend/handlers/handlers.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ type Backend interface {
101101
AOPPCancel()
102102
AOPPApprove()
103103
AOPPChooseAccount(code accountsTypes.Code)
104+
Authenticate(force bool)
105+
TriggerAuth()
106+
ForceAuth()
104107
GetAccountFromCode(code string) (accounts.Interface, error)
105108
HTTPClient() *http.Client
106109
CancelConnectKeystore()
@@ -194,6 +197,9 @@ func NewHandlers(
194197
getAPIRouterNoError(apiRouter)("/update", handlers.getUpdateHandler).Methods("GET")
195198
getAPIRouterNoError(apiRouter)("/banners/{key}", handlers.getBannersHandler).Methods("GET")
196199
getAPIRouterNoError(apiRouter)("/using-mobile-data", handlers.getUsingMobileDataHandler).Methods("GET")
200+
getAPIRouterNoError(apiRouter)("/authenticate", handlers.postAuthenticateHandler).Methods("POST")
201+
getAPIRouterNoError(apiRouter)("/trigger-auth", handlers.postTriggerAuthHandler).Methods("POST")
202+
getAPIRouterNoError(apiRouter)("/force-auth", handlers.postForceAuthHandler).Methods("POST")
197203
getAPIRouter(apiRouter)("/set-dark-theme", handlers.postDarkThemeHandler).Methods("POST")
198204
getAPIRouterNoError(apiRouter)("/detect-dark-theme", handlers.getDetectDarkThemeHandler).Methods("GET")
199205
getAPIRouterNoError(apiRouter)("/version", handlers.getVersionHandler).Methods("GET")
@@ -459,6 +465,29 @@ func (handlers *Handlers) getUsingMobileDataHandler(r *http.Request) interface{}
459465
return handlers.backend.Environment().UsingMobileData()
460466
}
461467

468+
func (handlers *Handlers) postAuthenticateHandler(r *http.Request) interface{} {
469+
var force bool
470+
if err := json.NewDecoder(r.Body).Decode(&force); err != nil {
471+
return map[string]interface{}{
472+
"success": false,
473+
"errorMessage": err.Error(),
474+
}
475+
}
476+
477+
handlers.backend.Authenticate(force)
478+
return nil
479+
}
480+
481+
func (handlers *Handlers) postTriggerAuthHandler(r *http.Request) interface{} {
482+
handlers.backend.TriggerAuth()
483+
return nil
484+
}
485+
486+
func (handlers *Handlers) postForceAuthHandler(r *http.Request) interface{} {
487+
handlers.backend.ForceAuth()
488+
return nil
489+
}
490+
462491
func (handlers *Handlers) postDarkThemeHandler(r *http.Request) (interface{}, error) {
463492
var isDark bool
464493
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: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
void onCancel();
20+
}
21+
22+
public static void showAuthenticationPrompt(FragmentActivity activity, AuthCallback callback) {
23+
Executor executor = ContextCompat.getMainExecutor(activity);
24+
BiometricPrompt biometricPrompt = new BiometricPrompt(activity, executor, new BiometricPrompt.AuthenticationCallback() {
25+
@Override
26+
public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
27+
super.onAuthenticationSucceeded(result);
28+
new Handler(Looper.getMainLooper()).post(callback::onSuccess);
29+
}
30+
31+
@Override
32+
public void onAuthenticationFailed() {
33+
super.onAuthenticationFailed();
34+
new Handler(Looper.getMainLooper()).post(callback::onFailure);
35+
}
36+
37+
@Override
38+
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
39+
if (errorCode == BiometricPrompt.ERROR_USER_CANCELED) {
40+
Util.log("Authentication error: user canceled");
41+
new Handler(Looper.getMainLooper()).post(callback::onCancel);
42+
} else {
43+
Util.log("Authentication error: " + errorCode + " - " + errString);
44+
new Handler(Looper.getMainLooper()).post(callback::onFailure);
45+
}
46+
super.onAuthenticationError(errorCode, errString);
47+
}
48+
});
49+
50+
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
51+
.setTitle("Authentication required")
52+
.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL |
53+
BiometricManager.Authenticators.BIOMETRIC_WEAK)
54+
.setConfirmationRequired(false)
55+
.build();
56+
biometricPrompt.authenticate(promptInfo);
57+
}
58+
}

0 commit comments

Comments
 (0)