From 460736a1515b436498413a877188df838b6ea3c9 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Fri, 27 Jun 2025 14:05:07 -0700 Subject: [PATCH] android: add All() to state store implementation (#673) Android has its own SharedPreferences-backed implementation of ipn.StateStore. Due to https://github.com/golang/go/issues/13445, we bundle the key list into a single primitive and unpack it in Go in our All() implementation. This also adds a compile-time check to prevent drift the interface. Updates tailscale/tailscale#15830 Signed-off-by: kari-ts --- .../src/main/java/com/tailscale/ipn/App.kt | 10 ++++++++ libtailscale/interfaces.go | 4 ++++ libtailscale/store.go | 24 +++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index fdbd295..67a0a62 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -248,6 +248,16 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { return getEncryptedPrefs().getString(prefKey, null) } + override fun getStateStoreKeysJSON(): String { + val prefix = "statestore-" + val keys = getEncryptedPrefs() + .getAll() + .keys + .filter { it.startsWith(prefix) } + .map { it.removePrefix(prefix) } + return org.json.JSONArray(keys).toString() + } + @Throws(IOException::class, GeneralSecurityException::class) fun getEncryptedPrefs(): SharedPreferences { val key = MasterKey.Builder(this).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build() diff --git a/libtailscale/interfaces.go b/libtailscale/interfaces.go index 5663698..44b9616 100644 --- a/libtailscale/interfaces.go +++ b/libtailscale/interfaces.go @@ -29,6 +29,10 @@ type AppContext interface { // at the given key, or returns empty string if unset. DecryptFromPref(key string) (string, error) + // GetStateStoreKeysJson retrieves all keys stored in the encrypted SharedPreferences, + // strips off the "statestore-" prefix, and returns them as a JSON array. + GetStateStoreKeysJSON() string + // GetOSVersion gets the Android version. GetOSVersion() (string, error) diff --git a/libtailscale/store.go b/libtailscale/store.go index 3496b5d..4cc2960 100644 --- a/libtailscale/store.go +++ b/libtailscale/store.go @@ -5,6 +5,8 @@ package libtailscale import ( "encoding/base64" + "encoding/json" + "iter" "tailscale.com/ipn" ) @@ -23,6 +25,28 @@ func newStateStore(appCtx AppContext) *stateStore { } } +func (s *stateStore) All() iter.Seq2[ipn.StateKey, []byte] { + rawJSON := s.appCtx.GetStateStoreKeysJSON() + var keys []string + if err := json.Unmarshal([]byte(rawJSON), &keys); err != nil { + return func(yield func(ipn.StateKey, []byte) bool) {} + } + return func(yield func(ipn.StateKey, []byte) bool) { + for _, k := range keys { + blob, err := s.ReadState(ipn.StateKey(k)) + if err != nil { + continue + } + if !yield(ipn.StateKey(k), blob) { + return + } + } + } +} + +// compile-time assertion that store must implement ipn.StateStore to give immediate feedback on interface drift. +var _ ipn.StateStore = (*stateStore)(nil) + func prefKeyFor(id ipn.StateKey) string { return "statestore-" + string(id) }