使用Google Tink加密OAuth2令牌时出现间歇性数据损坏导致刷新令牌失败的问题排查
Let’s break down the problems in your code that are almost certainly causing the intermittent invalid_grant errors when refreshing tokens:
1. Critical Base64 Decoding Bug in reveal()
Your biggest issue is a straightforward mistake in the reveal method:
val encrypted = Base64.decode(encrypted64.encodeToByteArray(), BASE64_ENCODE_SETTINGS)
Here, you’re taking the Base64-encoded string encrypted64, converting it to a UTF-8 byte array, and then trying to Base64-decode that byte array. This is completely incorrect—you should directly pass the encrypted64 string to Base64.decode() instead of converting it to bytes first.
This error means you’re decoding the wrong data entirely. Occasionally this might accidentally produce bytes that can be decrypted (leading to working tokens), but most of the time it will result in corrupted plaintext (like a garbage refresh token), which the server rejects with invalid_grant.
2. Redundant & Risky Base64 Encoding Steps
Your current flow adds unnecessary Base64 layers:
conceal(): Plaintext → Base64 → Encrypt → Base64reveal(): Encrypted Base64 → Decode → Decrypt → Base64 Decode
There’s no need to Base64-encode the plaintext before encryption. Tink’s Aead works directly with raw byte arrays, and adding this extra step introduces opportunities for encoding mismatches. For example, using NO_PADDING can lead to issues if any part of the pipeline expects padded Base64 (some servers or libraries handle unpadded Base64 poorly, causing intermittent failures).
3. Potential Base64 Flag Compatibility Issues
Using Base64.NO_WRAP or Base64.NO_PADDING might cause compatibility problems:
NO_WRAPis generally safe, butNO_PADDINGcan lead to decoding failures if the receiving end doesn’t correctly handle unpadded Base64. While unpadded Base64 is technically standard, not all implementations support it reliably, which could explain your intermittent failures.
Fixed Encryption/Decryption Code
Here’s the corrected version of your Security class that addresses all these issues:
import android.util.Base64 import com.google.crypto.tink.Aead import javax.inject.Inject // Use standard Base64 flags for better cross-component compatibility const val BASE64_ENCODE_SETTINGS = Base64.NO_WRAP data class Security @Inject constructor(private val authenticatedEncryption: Aead) { fun conceal(plainText: String, associatedData: String): String { // Encrypt raw plaintext bytes directly (no pre-Base64 needed) val plainBytes = plainText.encodeToByteArray() val associatedBytes = associatedData.encodeToByteArray() val encrypted = authenticatedEncryption.encrypt(plainBytes, associatedBytes) // Base64-encode encrypted bytes only for safe storage/transmission return Base64.encodeToString(encrypted, BASE64_ENCODE_SETTINGS) } fun reveal(encrypted64: String, associatedData: String): String { // Directly decode the Base64 string to get encrypted bytes val encrypted = Base64.decode(encrypted64, BASE64_ENCODE_SETTINGS) val associatedBytes = associatedData.encodeToByteArray() // Decrypt to get raw plaintext bytes val decrypted = authenticatedEncryption.decrypt(encrypted, associatedBytes) // Convert directly to string (no post-Base64 decode needed) return String(decrypted) } }
Additional Checks to Rule Out Other Issues
- Keyset Persistence: Ensure your
AndroidKeysetManagersetup is correctly persisting the keyset across app restarts. If keys are being regenerated unexpectedly, decryption of old tokens will fail. Your currentmanageTink()code looks valid, but double-check thatTINK_KEYSET_NAMEandPREF_FILE_NAMEare consistent throughout your app. - Associated Data Consistency: Make sure the
associatedDatapassed toconcealandrevealis exactly the same every time for the same token. If this data changes (e.g., using a dynamic value that varies between calls), decryption will fail outright.
These fixes should eliminate the intermittent decryption failures that are causing the invalid_grant errors from your OAuth server.
内容的提问来源于stack exchange,提问作者Hector

