Introduction
Authentication credentials are the keys to your kingdom. When OAuth tokens and API keys are stored without proper protection, you're essentially leaving those keys under the doormat. This vulnerability, recently patched in the docreader application, demonstrates a common but dangerous security oversight: storing sensitive authentication data in plaintext on the local filesystem.
For developers, this serves as a critical reminder that having security tools available isn't enough—you must actually use them. This case is particularly interesting because the application had PBKDF2 (Password-Based Key Derivation Function 2) available in its Rust dependencies but wasn't leveraging it to protect stored credentials.
The Vulnerability Explained
What Happened?
The vulnerability existed in the OAuth2 authentication plugin, specifically in the plugins/auth-oauth2/src/store.ts file. The getToken() and setToken() functions were responsible for managing OAuth tokens and API keys, but they performed these operations without any cryptographic protection:
// Vulnerable pattern (conceptual example)
function setToken(token: string) {
fs.writeFileSync('./credentials.json', JSON.stringify({ token }));
}
function getToken(): string {
const data = fs.readFileSync('./credentials.json', 'utf8');
return JSON.parse(data).token;
}
How Could It Be Exploited?
An attacker with access to the local filesystem could exploit this vulnerability in several ways:
- Direct File Access: Malware, physical access, or compromised backup systems could read the plaintext credential files
- Privilege Escalation: A low-privileged user on a shared system could access another user's tokens
- Forensic Recovery: Even deleted files could be recovered from disk, exposing historical credentials
- Cloud Sync Exposure: If the application directory syncs to cloud storage (Dropbox, OneDrive, etc.), tokens could leak to cloud servers
Real-World Impact
The consequences of this vulnerability include:
- Account Takeover: Stolen OAuth tokens allow attackers to impersonate users
- Data Breach: API keys could grant access to sensitive resources and data
- Lateral Movement: Compromised credentials could be used to access other connected services
- Compliance Violations: Plaintext credential storage violates PCI DSS, GDPR, and other regulatory requirements
Attack Scenario
Consider this realistic attack chain:
- A user installs the docreader application and authenticates with their Google account
- The OAuth token is stored in plaintext in
~/.docreader/auth/credentials.json - The user's system is infected with info-stealing malware (a common threat)
- The malware scans common application directories and finds the plaintext token
- The attacker uses the stolen token to access the victim's Google account, email, and connected services
- The victim doesn't notice because the token is still valid—no re-authentication is required
The Fix
What Should Have Been Done?
The proper solution involves encrypting credentials before storing them on disk. Since PBKDF2 was already available in the Rust dependencies (src-tauri/Cargo.lock:3809), the fix should implement encryption using this proven cryptographic function.
Recommended Implementation
Here's how the fix should look conceptually:
Before (Vulnerable):
// Plaintext storage - INSECURE
function setToken(token: string) {
const data = { token, timestamp: Date.now() };
fs.writeFileSync(TOKEN_PATH, JSON.stringify(data));
}
function getToken(): string | null {
if (!fs.existsSync(TOKEN_PATH)) return null;
const data = JSON.parse(fs.readFileSync(TOKEN_PATH, 'utf8'));
return data.token;
}
After (Secure):
import { invoke } from '@tauri-apps/api/tauri';
// Encrypted storage using Rust backend with PBKDF2
async function setToken(token: string) {
// Derive encryption key from user-specific data
const encryptedToken = await invoke('encrypt_token', {
plaintext: token
});
fs.writeFileSync(TOKEN_PATH, encryptedToken);
}
async function getToken(): Promise<string | null> {
if (!fs.existsSync(TOKEN_PATH)) return null;
const encryptedToken = fs.readFileSync(TOKEN_PATH, 'utf8');
// Decrypt using Rust backend
const token = await invoke('decrypt_token', {
ciphertext: encryptedToken
});
return token;
}
Rust Backend (src-tauri/src/crypto.rs):
use pbkdf2::{pbkdf2_hmac};
use aes_gcm::{Aes256Gcm, Key, Nonce};
use aes_gcm::aead::{Aead, NewAead};
use sha2::Sha256;
#[tauri::command]
fn encrypt_token(plaintext: String) -> Result<String, String> {
// Derive key using PBKDF2
let salt = get_device_specific_salt(); // Hardware-based or OS keychain
let mut key = [0u8; 32];
pbkdf2_hmac::<Sha256>(
get_master_password().as_bytes(),
&salt,
100_000, // iterations
&mut key
);
// Encrypt using AES-256-GCM
let cipher = Aes256Gcm::new(Key::from_slice(&key));
let nonce = Nonce::from_slice(b"unique nonce"); // Generate proper nonce
let ciphertext = cipher.encrypt(nonce, plaintext.as_bytes())
.map_err(|e| e.to_string())?;
Ok(base64::encode(ciphertext))
}
#[tauri::command]
fn decrypt_token(ciphertext: String) -> Result<String, String> {
// Similar decryption logic
// ...
}
Security Improvements
This fix provides multiple layers of protection:
- Encryption at Rest: Credentials are encrypted before touching the filesystem
- Key Derivation: PBKDF2 with 100,000+ iterations makes brute-force attacks computationally expensive
- Device-Specific Protection: Using hardware-based or OS keychain data ties encryption to the specific device
- Authenticated Encryption: AES-GCM provides both confidentiality and integrity
Prevention & Best Practices
How to Avoid This Vulnerability
-
Never Store Secrets in Plaintext: This is security 101, yet it remains a common mistake
-
Use Platform Keychains: Leverage OS-provided secure storage:
- macOS: Keychain Services
- Windows: Credential Manager (DPAPI)
- Linux: Secret Service API (libsecret) -
Implement Defense in Depth:
typescript // Use multiple layers of protection - Encryption at rest (PBKDF2 + AES) - Restrictive file permissions (chmod 600) - OS keychain integration - Token rotation policies - Short-lived tokens when possible -
Apply the Principle of Least Privilege: Store only what you absolutely need
Security Recommendations
For OAuth Token Management:
- Use refresh tokens instead of long-lived access tokens
- Implement token rotation on every use
- Set appropriate token expiration times
- Clear tokens on logout
- Never log tokens (even in debug mode)
For API Key Storage:
- Use environment variables for server-side applications
- Use secure vaults (HashiCorp Vault, AWS Secrets Manager) in production
- Implement key rotation policies
- Monitor for unauthorized key usage
Detection Tools
Prevent this vulnerability using:
-
Static Analysis:
- GitLeaks: Detect hardcoded secrets in code
- TruffleHog: Scan git repositories for credentials
- Semgrep: Custom rules for insecure storage patterns -
Code Review Checklist:
☐ Are credentials encrypted before storage? ☐ Is a strong encryption algorithm used (AES-256)? ☐ Is proper key derivation implemented (PBKDF2, Argon2)? ☐ Are file permissions restrictive? ☐ Is the OS keychain used when available? -
Runtime Monitoring:
- File integrity monitoring (FIM)
- Audit logs for credential access
- Anomaly detection for unusual file access patterns
Security Standards & References
- CWE-312: Cleartext Storage of Sensitive Information
- CWE-522: Insufficiently Protected Credentials
- OWASP Top 10 2021: A07:2021 – Identification and Authentication Failures
- OWASP MASVS: MSTG-STORAGE-1 (Secure Storage)
- PCI DSS: Requirement 3.4 (Render PAN unreadable)
- NIST SP 800-63B: Digital Identity Guidelines (Authentication)
Conclusion
The plaintext credential storage vulnerability in docreader serves as an important lesson: security dependencies are useless if you don't use them. Having PBKDF2 available in the project's dependencies didn't protect users—only proper implementation could do that.
Key takeaways for developers:
- Encrypt sensitive data at rest—always, no exceptions
- Leverage existing security libraries rather than rolling your own crypto
- Use platform-specific secure storage when available
- Implement defense in depth with multiple security layers
- Regular security audits can catch these issues before they reach production
Remember: in security, good intentions aren't enough. The best encryption algorithm in the world provides zero protection if it's never invoked. Review your own applications today—are your credentials truly protected, or are they just one filesystem access away from compromise?
Stay secure, and always encrypt your secrets! 🔐
Have questions about secure credential storage? Found a similar vulnerability in your codebase? Share your experiences in the comments below.