feat(android): Android Keystore AES-GCM token storage via JNI

Replaces the four KeychainUnavailable stubs in auth_tokens.rs with a
real Android Keystore implementation:

- Device-bound AES-256/GCM/NoPadding key under alias
  'solitaire_quest_token_key'; generated on first use, survives
  restarts, destroyed on uninstall.
- Tokens serialised as JSON, encrypted to
  {data_dir}/auth_tokens.bin as [12-byte IV][ciphertext+GCM-tag];
  writes are atomic (tmp → rename).
- Key invalidation (biometric/lock change) surfaces as
  TokenError::KeychainUnavailable, matching desktop fallback semantics.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-05-08 21:05:20 -07:00
parent 2c822ba2d7
commit f281425b45
4 changed files with 429 additions and 17 deletions
+11 -17
View File
@@ -131,35 +131,29 @@ pub fn delete_tokens(username: &str) -> Result<(), TokenError> {
}
// -------------------------------------------------------------------
// Android stub — same public API, always returns KeychainUnavailable.
// Lets `sync_client::*` compile unchanged on Android; the runtime
// effect is "session login required every launch", same as a Linux
// box without Secret Service.
// Android — delegate to the JNI Keystore bridge in android_keystore.
// -------------------------------------------------------------------
#[cfg(target_os = "android")]
const ANDROID_STUB_MSG: &str = "android stub: keychain not yet wired (Phase-Android task)";
#[cfg(target_os = "android")]
pub fn store_tokens(
_username: &str,
_access_token: &str,
_refresh_token: &str,
username: &str,
access_token: &str,
refresh_token: &str,
) -> Result<(), TokenError> {
Err(TokenError::KeychainUnavailable(ANDROID_STUB_MSG.to_string()))
crate::android_keystore::store_tokens(username, access_token, refresh_token)
}
#[cfg(target_os = "android")]
pub fn load_access_token(_username: &str) -> Result<String, TokenError> {
Err(TokenError::KeychainUnavailable(ANDROID_STUB_MSG.to_string()))
pub fn load_access_token(username: &str) -> Result<String, TokenError> {
crate::android_keystore::load_access_token(username)
}
#[cfg(target_os = "android")]
pub fn load_refresh_token(_username: &str) -> Result<String, TokenError> {
Err(TokenError::KeychainUnavailable(ANDROID_STUB_MSG.to_string()))
pub fn load_refresh_token(username: &str) -> Result<String, TokenError> {
crate::android_keystore::load_refresh_token(username)
}
#[cfg(target_os = "android")]
pub fn delete_tokens(_username: &str) -> Result<(), TokenError> {
Err(TokenError::KeychainUnavailable(ANDROID_STUB_MSG.to_string()))
pub fn delete_tokens(username: &str) -> Result<(), TokenError> {
crate::android_keystore::delete_tokens(username)
}