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) }