// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package main import ( "encoding/base64" "tailscale.com/ipn" "github.com/tailscale/tailscale-android/cmd/jni" ) // stateStore is the Go interface for a persistent storage // backend by androidx.security.crypto.EncryptedSharedPreferences (see // App.java). type stateStore struct { jvm *jni.JVM // appCtx is the global Android app context. appCtx jni.Object // Cached method ids on appCtx. encrypt jni.MethodID decrypt jni.MethodID } func newStateStore(jvm *jni.JVM, appCtx jni.Object) *stateStore { s := &stateStore{ jvm: jvm, appCtx: appCtx, } jni.Do(jvm, func(env *jni.Env) error { appCls := jni.GetObjectClass(env, appCtx) s.encrypt = jni.GetMethodID( env, appCls, "encryptToPref", "(Ljava/lang/String;Ljava/lang/String;)V", ) s.decrypt = jni.GetMethodID( env, appCls, "decryptFromPref", "(Ljava/lang/String;)Ljava/lang/String;", ) return nil }) return s } func prefKeyFor(id ipn.StateKey) string { return "statestore-" + string(id) } func (s *stateStore) ReadString(key string, def string) (string, error) { data, err := s.read(key) if err != nil { return def, err } if data == nil { return def, nil } return string(data), nil } func (s *stateStore) WriteString(key string, val string) error { return s.write(key, []byte(val)) } func (s *stateStore) ReadBool(key string, def bool) (bool, error) { data, err := s.read(key) if err != nil { return def, err } if data == nil { return def, nil } return string(data) == "true", nil } func (s *stateStore) WriteBool(key string, val bool) error { data := []byte("false") if val { data = []byte("true") } return s.write(key, data) } func (s *stateStore) ReadState(id ipn.StateKey) ([]byte, error) { state, err := s.read(prefKeyFor(id)) if err != nil { return nil, err } if state == nil { return nil, ipn.ErrStateNotExist } return state, nil } func (s *stateStore) WriteState(id ipn.StateKey, bs []byte) error { prefKey := prefKeyFor(id) return s.write(prefKey, bs) } func (s *stateStore) read(key string) ([]byte, error) { var data []byte err := jni.Do(s.jvm, func(env *jni.Env) error { jfile := jni.JavaString(env, key) plain, err := jni.CallObjectMethod(env, s.appCtx, s.decrypt, jni.Value(jfile)) if err != nil { return err } b64 := jni.GoString(env, jni.String(plain)) if b64 == "" { return nil } data, err = base64.RawStdEncoding.DecodeString(b64) return err }) return data, err } func (s *stateStore) write(key string, value []byte) error { bs64 := base64.RawStdEncoding.EncodeToString(value) err := jni.Do(s.jvm, func(env *jni.Env) error { jfile := jni.JavaString(env, key) jplain := jni.JavaString(env, bs64) err := jni.CallVoidMethod(env, s.appCtx, s.encrypt, jni.Value(jfile), jni.Value(jplain)) if err != nil { return err } return nil }) return err }