LibriSync Security Analysis
Stack: React Native / Expo (TypeScript) + Rust native core (Android JNI) + SQLite storage + Audible OAuth/API
CRITICAL
- OAuth Credentials Logged to stderr — native/rust-core/src/api/auth.rs:1399-1406
exchange_authorization_code() unconditionally prints the complete device registration request body via eprintln!, which includes the authorization_code and the PKCE code_verifier. On Android these go to logcat, readable by any app with READ_LOGS permission (or developer tools without root in debug builds).
eprintln!("=== Device Registration Request ===");
eprintln!("URL: {}", register_url);
eprintln!("Body: {}", serde_json::to_string_pretty(&request_body)...); // contains auth code + verifier
Fix: Remove these eprintln! blocks entirely. The registration URL is not sensitive, but the body is.
HIGH
2. DRM Decryption Key Logged in Plaintext — native/rust-core/src/api/license.rs:422, 433-437
After decrypting a license voucher, the raw decrypted JSON (which contains the AES key and IV used to decrypt audiobooks) is printed to stderr:
eprintln!("🔍 DEBUG: Decrypted voucher JSON:\n{}\n", json_str);
// ...
eprintln!("🔍 DEBUG: Voucher key length: {}, iv length: {:?}", voucher.key.len(), ...);
The first statement logs the actual encryption key. Fix: Remove both debug eprintln! calls.
MEDIUM
3. OAuth State Parameter Omitted (CSRF) — native/rust-core/src/api/auth.rs:1152-1154
The state parameter is intentionally omitted from the authorization URL with the comment "Amazon OAuth may not support state parameter in this flow." This removes the standard CSRF protection for the OAuth callback. An attacker who can control a redirect (e.g., via malicious deep link or phishing) could potentially inject a forged authorization code. The OAuthState struct exists and is generated but never sent.
Fix: Send the state parameter and validate it in parse_authorization_callback(). Even if Amazon ignores it, it costs nothing.
- Credentials Stored Unencrypted at Rest — native/rust-core/src/storage/accounts.rs:51-77
The identity_json column in the Accounts table stores the full OAuth identity blob — including access_token, refresh_token, device_private_key, adp_token, and session cookies — in plaintext SQLite. This is acknowledged in a comment in auth.rs as a "future enhancement." On a rooted Android device, or via a local backup, these credentials would be fully exposed.
Fix (minimum): On Android, use the Jetpack Security EncryptedFile API or SQLCipher to encrypt the database. At minimum, encrypt the identity_json column using a key stored in the Android Keystore.
- WebView Allows Mixed Content — src/components/OAuthWebView.tsx:170
mixedContentMode="always"
This permits the OAuth WebView to load HTTP subresources inside the HTTPS Amazon login page. A network attacker (e.g., café Wi-Fi) could inject content into the authentication page. The default ("never") should be used.
Fix: Remove mixedContentMode="always" or change it to "never".
LOW
6. Test Fixture Contains Real User PII — native/rust-core/test_fixtures/registration_response.json
The fixture file contains what appear to be a real Amazon account ID (amzn1.account.AGMGLSGIFYVALF2MEO4F3JJQRLSA), full name ("Henning Berge"), and a real device serial number. The tokens themselves are clearly synthetic, but the PII and device serial are real and committed to the repository. If this repository is or becomes public, that data is exposed.
Fix: Replace the customer_info name/user_id and device_serial_number with entirely synthetic values.
- API Error Bodies Reflected in Error Messages — native/rust-core/src/api/client.rs:706-722
handle_success_response() includes up to 800 characters of the response body in JSON parse error messages, and handle_error_response() includes the entire error body. If an API response contains partial token data (e.g., a truncated JSON response), this could surface in crash reports or logs.
Fix: Log the response body separately at DEBUG level; return a generic error message to callers.
- Math.random() for Cryptographic Purposes (Dead Code) — src/components/OAuthWebView.tsx:183-204
generateDeviceSerial() and generateRandomString() use Math.random(), which is not cryptographically secure. These functions are currently unreachable (the actual OAuth URL is generated by the Rust bridge), but their presence is a trap. If a developer calls them, device serials or PKCE verifiers would be predictable.
Fix: Delete these dead functions, or replace Math.random() with crypto.getRandomValues().
- No TLS Certificate Pinning
The reqwest client has no certificate pinning configured. A compromised CA or MITM on a corporate network could intercept Audible API traffic including token exchanges.
Fix: For higher-assurance deployments, add Audible's certificate chain via reqwest::ClientBuilder::add_root_certificate() or platform-level pinning. This is optional for most personal-use apps.
Summary Table
Severity Location Issue
1 Critical auth.rs:1399 OAuth auth code + PKCE verifier logged to stderr
2 High license.rs:422 DRM decryption key logged in plaintext
3 Medium auth.rs:1152 OAuth state/CSRF parameter omitted
4 Medium accounts.rs Credentials stored unencrypted in SQLite
5 Medium OAuthWebView.tsx:170 mixedContentMode="always" in auth WebView
6 Low test_fixtures/…json Real user PII and device serial in repo
7 Low client.rs:706 API error bodies reflected in error messages
8 Low OAuthWebView.tsx:183 Math.random() for crypto (dead code)
9 Low client.rs No TLS certificate pinning
LibriSync Security Analysis
Stack: React Native / Expo (TypeScript) + Rust native core (Android JNI) + SQLite storage + Audible OAuth/API
CRITICAL
exchange_authorization_code() unconditionally prints the complete device registration request body via eprintln!, which includes the authorization_code and the PKCE code_verifier. On Android these go to logcat, readable by any app with READ_LOGS permission (or developer tools without root in debug builds).
eprintln!("=== Device Registration Request ===");
eprintln!("URL: {}", register_url);
eprintln!("Body: {}", serde_json::to_string_pretty(&request_body)...); // contains auth code + verifier
Fix: Remove these eprintln! blocks entirely. The registration URL is not sensitive, but the body is.
HIGH
2. DRM Decryption Key Logged in Plaintext — native/rust-core/src/api/license.rs:422, 433-437
After decrypting a license voucher, the raw decrypted JSON (which contains the AES key and IV used to decrypt audiobooks) is printed to stderr:
eprintln!("🔍 DEBUG: Decrypted voucher JSON:\n{}\n", json_str);
// ...
eprintln!("🔍 DEBUG: Voucher key length: {}, iv length: {:?}", voucher.key.len(), ...);
The first statement logs the actual encryption key. Fix: Remove both debug eprintln! calls.
MEDIUM
3. OAuth State Parameter Omitted (CSRF) — native/rust-core/src/api/auth.rs:1152-1154
The state parameter is intentionally omitted from the authorization URL with the comment "Amazon OAuth may not support state parameter in this flow." This removes the standard CSRF protection for the OAuth callback. An attacker who can control a redirect (e.g., via malicious deep link or phishing) could potentially inject a forged authorization code. The OAuthState struct exists and is generated but never sent.
Fix: Send the state parameter and validate it in parse_authorization_callback(). Even if Amazon ignores it, it costs nothing.
The identity_json column in the Accounts table stores the full OAuth identity blob — including access_token, refresh_token, device_private_key, adp_token, and session cookies — in plaintext SQLite. This is acknowledged in a comment in auth.rs as a "future enhancement." On a rooted Android device, or via a local backup, these credentials would be fully exposed.
Fix (minimum): On Android, use the Jetpack Security EncryptedFile API or SQLCipher to encrypt the database. At minimum, encrypt the identity_json column using a key stored in the Android Keystore.
mixedContentMode="always"
This permits the OAuth WebView to load HTTP subresources inside the HTTPS Amazon login page. A network attacker (e.g., café Wi-Fi) could inject content into the authentication page. The default ("never") should be used.
Fix: Remove mixedContentMode="always" or change it to "never".
LOW
6. Test Fixture Contains Real User PII — native/rust-core/test_fixtures/registration_response.json
The fixture file contains what appear to be a real Amazon account ID (amzn1.account.AGMGLSGIFYVALF2MEO4F3JJQRLSA), full name ("Henning Berge"), and a real device serial number. The tokens themselves are clearly synthetic, but the PII and device serial are real and committed to the repository. If this repository is or becomes public, that data is exposed.
Fix: Replace the customer_info name/user_id and device_serial_number with entirely synthetic values.
handle_success_response() includes up to 800 characters of the response body in JSON parse error messages, and handle_error_response() includes the entire error body. If an API response contains partial token data (e.g., a truncated JSON response), this could surface in crash reports or logs.
Fix: Log the response body separately at DEBUG level; return a generic error message to callers.
generateDeviceSerial() and generateRandomString() use Math.random(), which is not cryptographically secure. These functions are currently unreachable (the actual OAuth URL is generated by the Rust bridge), but their presence is a trap. If a developer calls them, device serials or PKCE verifiers would be predictable.
Fix: Delete these dead functions, or replace Math.random() with crypto.getRandomValues().
The reqwest client has no certificate pinning configured. A compromised CA or MITM on a corporate network could intercept Audible API traffic including token exchanges.
Fix: For higher-assurance deployments, add Audible's certificate chain via reqwest::ClientBuilder::add_root_certificate() or platform-level pinning. This is optional for most personal-use apps.
Summary Table
Severity Location Issue
1 Critical auth.rs:1399 OAuth auth code + PKCE verifier logged to stderr
2 High license.rs:422 DRM decryption key logged in plaintext
3 Medium auth.rs:1152 OAuth state/CSRF parameter omitted
4 Medium accounts.rs Credentials stored unencrypted in SQLite
5 Medium OAuthWebView.tsx:170 mixedContentMode="always" in auth WebView
6 Low test_fixtures/…json Real user PII and device serial in repo
7 Low client.rs:706 API error bodies reflected in error messages
8 Low OAuthWebView.tsx:183 Math.random() for crypto (dead code)
9 Low client.rs No TLS certificate pinning