Skip to content

Commit 8117878

Browse files
Add files via upload
1 parent 27346e9 commit 8117878

1 file changed

Lines changed: 62 additions & 133 deletions

File tree

app/src/main/kotlin/com/google/ai/sample/TrialManager.kt

Lines changed: 62 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -3,139 +3,63 @@ package com.google.ai.sample
33
import android.content.Context
44
import android.content.SharedPreferences
55
import android.os.Build
6-
import android.security.keystore.KeyGenParameterSpec
7-
import android.security.keystore.KeyProperties
8-
import android.util.Base64
96
import android.util.Log
10-
import java.nio.charset.Charset
11-
import java.security.KeyStore
12-
import javax.crypto.Cipher
13-
import javax.crypto.KeyGenerator
14-
import javax.crypto.SecretKey
15-
import javax.crypto.spec.GCMParameterSpec
167

178
object TrialManager {
189

1910
private const val PREFS_NAME = "TrialPrefs"
2011
const val TRIAL_DURATION_MS = 30 * 60 * 1000L // 30 minutes in milliseconds
2112

22-
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
23-
private const val KEY_ALIAS_TRIAL_END_TIME_KEY = "TrialEndTimeEncryptionKeyAlias"
24-
private const val KEY_ENCRYPTED_TRIAL_UTC_END_TIME = "encryptedTrialUtcEndTime"
25-
private const val KEY_ENCRYPTION_IV = "encryptionIv"
13+
// Key for storing the trial end time as a plain Long (unencrypted)
14+
private const val KEY_TRIAL_END_TIME_UNENCRYPTED = "trialUtcEndTimeUnencrypted"
2615
private const val KEY_TRIAL_AWAITING_FIRST_INTERNET_TIME = "trialAwaitingFirstInternetTime"
2716
private const val KEY_PURCHASED_FLAG = "appPurchased"
2817

2918
private const val TAG = "TrialManager"
3019

31-
// AES/GCM/NoPadding is a good choice for symmetric encryption
32-
private const val ENCRYPTION_TRANSFORMATION = "AES/GCM/NoPadding"
33-
private const val ENCRYPTION_BLOCK_SIZE = 12 // GCM IV size is typically 12 bytes
20+
// Keystore and encryption related constants are no longer used for storing trial end time
21+
// but kept here in case they are used for other purposes or future reinstatement.
22+
// private const val ANDROID_KEYSTORE = "AndroidKeyStore"
23+
// private const val KEY_ALIAS_TRIAL_END_TIME_KEY = "TrialEndTimeEncryptionKeyAlias"
24+
// private const val KEY_ENCRYPTED_TRIAL_UTC_END_TIME = "encryptedTrialUtcEndTime" // No longer used for saving
25+
// private const val KEY_ENCRYPTION_IV = "encryptionIv" // No longer used for saving
26+
// private const val ENCRYPTION_TRANSFORMATION = "AES/GCM/NoPadding"
27+
// private const val ENCRYPTION_BLOCK_SIZE = 12
3428

3529
enum class TrialState {
3630
NOT_YET_STARTED_AWAITING_INTERNET,
3731
ACTIVE_INTERNET_TIME_CONFIRMED,
3832
EXPIRED_INTERNET_TIME_CONFIRMED,
3933
PURCHASED,
40-
INTERNET_UNAVAILABLE_CANNOT_VERIFY // Used when current internet time is not available
34+
INTERNET_UNAVAILABLE_CANNOT_VERIFY
4135
}
4236

4337
private fun getSharedPreferences(context: Context): SharedPreferences {
4438
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
4539
}
4640

47-
private fun getKeyStore(): KeyStore {
48-
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
49-
keyStore.load(null)
50-
return keyStore
51-
}
52-
53-
private fun getOrCreateSecretKey(): SecretKey {
54-
val keyStore = getKeyStore()
55-
if (!keyStore.containsAlias(KEY_ALIAS_TRIAL_END_TIME_KEY)) {
56-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
57-
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE)
58-
val parameterSpec = KeyGenParameterSpec.Builder(
59-
KEY_ALIAS_TRIAL_END_TIME_KEY,
60-
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
61-
)
62-
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
63-
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
64-
.setKeySize(256) // AES-256
65-
.build()
66-
keyGenerator.init(parameterSpec)
67-
return keyGenerator.generateKey()
68-
} else {
69-
throw SecurityException("KeyStore encryption for trial end time not supported on this API level for key generation.")
70-
}
71-
}
72-
return keyStore.getKey(KEY_ALIAS_TRIAL_END_TIME_KEY, null) as SecretKey
73-
}
74-
75-
private fun saveEncryptedTrialUtcEndTime(context: Context, utcEndTimeMs: Long) {
76-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
77-
Log.w(TAG, "Skipping KeyStore encryption for API < 23. Storing end time in plain text (less secure).")
78-
getSharedPreferences(context).edit().putLong(KEY_ENCRYPTED_TRIAL_UTC_END_TIME, utcEndTimeMs).apply()
79-
return
80-
}
81-
try {
82-
val secretKey = getOrCreateSecretKey()
83-
val cipher = Cipher.getInstance(ENCRYPTION_TRANSFORMATION)
84-
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
85-
val iv = cipher.iv
86-
val encryptedEndTime = cipher.doFinal(utcEndTimeMs.toString().toByteArray(Charset.defaultCharset()))
87-
88-
val editor = getSharedPreferences(context).edit()
89-
editor.putString(KEY_ENCRYPTED_TRIAL_UTC_END_TIME, Base64.encodeToString(encryptedEndTime, Base64.DEFAULT))
90-
editor.putString(KEY_ENCRYPTION_IV, Base64.encodeToString(iv, Base64.DEFAULT))
91-
editor.apply()
92-
Log.d(TAG, "Encrypted and saved UTC end time.")
93-
} catch (e: Exception) {
94-
Log.e(TAG, "Error encrypting or saving trial end time with KeyStore", e)
95-
getSharedPreferences(context).edit().putLong(KEY_ENCRYPTED_TRIAL_UTC_END_TIME + "_unencrypted_fallback", utcEndTimeMs).apply()
96-
Log.w(TAG, "Saved unencrypted fallback UTC end time due to KeyStore error.")
97-
}
41+
// Simplified function to save trial end time as a plain Long
42+
private fun saveTrialUtcEndTime(context: Context, utcEndTimeMs: Long) {
43+
val editor = getSharedPreferences(context).edit()
44+
editor.putLong(KEY_TRIAL_END_TIME_UNENCRYPTED, utcEndTimeMs)
45+
editor.apply()
46+
Log.d(TAG, "Saved unencrypted UTC end time: $utcEndTimeMs")
9847
}
9948

100-
private fun getDecryptedTrialUtcEndTime(context: Context): Long? {
49+
// Simplified function to get trial end time as a plain Long
50+
private fun getTrialUtcEndTime(context: Context): Long? {
10151
val prefs = getSharedPreferences(context)
102-
val encryptedEndTimeString = prefs.getString(KEY_ENCRYPTED_TRIAL_UTC_END_TIME, null)
103-
val ivString = prefs.getString(KEY_ENCRYPTION_IV, null)
104-
105-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
106-
Log.w(TAG, "Skipping KeyStore decryption for API < 23. Reading plain text end time.")
107-
return if(prefs.contains(KEY_ENCRYPTED_TRIAL_UTC_END_TIME)) prefs.getLong(KEY_ENCRYPTED_TRIAL_UTC_END_TIME, 0L) else null
108-
}
109-
110-
if (encryptedEndTimeString == null || ivString == null) {
111-
if (prefs.contains(KEY_ENCRYPTED_TRIAL_UTC_END_TIME + "_unencrypted_fallback")) {
112-
Log.w(TAG, "Using unencrypted fallback for end time as encrypted version not found.")
113-
return prefs.getLong(KEY_ENCRYPTED_TRIAL_UTC_END_TIME + "_unencrypted_fallback", 0L)
114-
}
115-
Log.d(TAG, "No encrypted end time or IV found, and no fallback.")
52+
if (!prefs.contains(KEY_TRIAL_END_TIME_UNENCRYPTED)) {
53+
Log.d(TAG, "No unencrypted trial end time found.")
11654
return null
11755
}
118-
119-
return try {
120-
val secretKey = getOrCreateSecretKey()
121-
val encryptedEndTime = Base64.decode(encryptedEndTimeString, Base64.DEFAULT)
122-
val iv = Base64.decode(ivString, Base64.DEFAULT)
123-
124-
val cipher = Cipher.getInstance(ENCRYPTION_TRANSFORMATION)
125-
val spec = GCMParameterSpec(128, iv)
126-
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
127-
val decryptedBytes = cipher.doFinal(encryptedEndTime)
128-
val decryptedString = String(decryptedBytes, Charset.defaultCharset())
129-
Log.d(TAG, "Decrypted UTC end time successfully.")
130-
decryptedString.toLongOrNull()
131-
} catch (e: Exception) {
132-
Log.e(TAG, "Error decrypting trial end time with KeyStore", e)
133-
// If decryption fails, try to use the fallback if it exists
134-
if (prefs.contains(KEY_ENCRYPTED_TRIAL_UTC_END_TIME + "_unencrypted_fallback")) {
135-
Log.w(TAG, "Using unencrypted fallback for end time due to decryption error.")
136-
return prefs.getLong(KEY_ENCRYPTED_TRIAL_UTC_END_TIME + "_unencrypted_fallback", 0L)
137-
}
56+
val endTime = prefs.getLong(KEY_TRIAL_END_TIME_UNENCRYPTED, -1L)
57+
return if (endTime == -1L) {
58+
Log.w(TAG, "Found unencrypted end time key, but value was -1L, treating as not found.")
13859
null
60+
} else {
61+
Log.d(TAG, "Retrieved unencrypted UTC end time: $endTime")
62+
endTime
13963
}
14064
}
14165

@@ -146,14 +70,15 @@ object TrialManager {
14670
return
14771
}
14872
// Only start if no end time is set AND we are awaiting the first internet time.
149-
if (getDecryptedTrialUtcEndTime(context) == null && prefs.getBoolean(KEY_TRIAL_AWAITING_FIRST_INTERNET_TIME, true)) {
73+
if (getTrialUtcEndTime(context) == null && prefs.getBoolean(KEY_TRIAL_AWAITING_FIRST_INTERNET_TIME, true)) {
15074
val utcEndTimeMs = currentUtcTimeMs + TRIAL_DURATION_MS
151-
saveEncryptedTrialUtcEndTime(context, utcEndTimeMs)
75+
saveTrialUtcEndTime(context, utcEndTimeMs) // Use simplified save function
15276
// Crucially, set awaiting flag to false *after* attempting to save.
15377
prefs.edit().putBoolean(KEY_TRIAL_AWAITING_FIRST_INTERNET_TIME, false).apply()
154-
Log.i(TAG, "Trial started with internet time. Ends at UTC: $utcEndTimeMs. Awaiting flag set to false.")
78+
Log.i(TAG, "Trial started with internet time (unencrypted). Ends at UTC: $utcEndTimeMs. Awaiting flag set to false.")
15579
} else {
156-
Log.d(TAG, "Trial already started or not awaiting first internet time (or end time already exists).")
80+
val existingEndTime = getTrialUtcEndTime(context)
81+
Log.d(TAG, "Trial not started: Existing EndTime: $existingEndTime, AwaitingInternet: ${prefs.getBoolean(KEY_TRIAL_AWAITING_FIRST_INTERNET_TIME, true)}")
15782
}
15883
}
15984

@@ -164,59 +89,60 @@ object TrialManager {
16489

16590
val prefs = getSharedPreferences(context)
16691
val isAwaitingFirstInternetTime = prefs.getBoolean(KEY_TRIAL_AWAITING_FIRST_INTERNET_TIME, true)
167-
val decryptedUtcEndTime = getDecryptedTrialUtcEndTime(context)
92+
val trialUtcEndTime = getTrialUtcEndTime(context) // Use simplified get function
16893

16994
if (currentUtcTimeMs == null) {
170-
return if (decryptedUtcEndTime == null && isAwaitingFirstInternetTime) {
95+
return if (trialUtcEndTime == null && isAwaitingFirstInternetTime) {
17196
TrialState.NOT_YET_STARTED_AWAITING_INTERNET
17297
} else {
173-
// If end time exists, or if not awaiting, but no internet, we can't verify.
17498
TrialState.INTERNET_UNAVAILABLE_CANNOT_VERIFY
17599
}
176100
}
177-
// currentUtcTimeMs is NOT null from here
101+
178102
return when {
179-
decryptedUtcEndTime == null && isAwaitingFirstInternetTime -> TrialState.NOT_YET_STARTED_AWAITING_INTERNET
180-
decryptedUtcEndTime == null && !isAwaitingFirstInternetTime -> {
181-
// This means KEY_TRIAL_AWAITING_FIRST_INTERNET_TIME was set to false (trial start was attempted),
182-
// but we couldn't retrieve/decrypt an end time. This is an error in persistence.
183-
// Do NOT reset KEY_TRIAL_AWAITING_FIRST_INTERNET_TIME to true, as this causes the "Warte auf..." loop.
103+
trialUtcEndTime == null && isAwaitingFirstInternetTime -> TrialState.NOT_YET_STARTED_AWAITING_INTERNET
104+
trialUtcEndTime == null && !isAwaitingFirstInternetTime -> {
184105
Log.e(TAG, "CRITICAL INCONSISTENCY: Trial marked as started (not awaiting internet), but no trial end time found. Check save/load logic. Returning INTERNET_UNAVAILABLE_CANNOT_VERIFY.")
185106
TrialState.INTERNET_UNAVAILABLE_CANNOT_VERIFY
186107
}
187-
decryptedUtcEndTime != null && currentUtcTimeMs < decryptedUtcEndTime -> TrialState.ACTIVE_INTERNET_TIME_CONFIRMED
188-
decryptedUtcEndTime != null && currentUtcTimeMs >= decryptedUtcEndTime -> TrialState.EXPIRED_INTERNET_TIME_CONFIRMED
108+
trialUtcEndTime != null && currentUtcTimeMs < trialUtcEndTime -> TrialState.ACTIVE_INTERNET_TIME_CONFIRMED
109+
trialUtcEndTime != null && currentUtcTimeMs >= trialUtcEndTime -> TrialState.EXPIRED_INTERNET_TIME_CONFIRMED
189110
else -> {
190-
// This case should ideally not be reached if logic above is exhaustive.
191-
// Could happen if decryptedUtcEndTime is null but isAwaitingFirstInternetTime is false (covered above)
192-
// or some other unexpected combination.
193-
Log.e(TAG, "Unhandled case in getTrialState. isAwaiting: $isAwaitingFirstInternetTime, endTime: $decryptedUtcEndTime. Defaulting to NOT_YET_STARTED_AWAITING_INTERNET.")
194-
TrialState.NOT_YET_STARTED_AWAITING_INTERNET // Fallback, but should be investigated if hit.
111+
Log.e(TAG, "Unhandled case in getTrialState. isAwaiting: $isAwaitingFirstInternetTime, endTime: $trialUtcEndTime. Defaulting to NOT_YET_STARTED_AWAITING_INTERNET.")
112+
TrialState.NOT_YET_STARTED_AWAITING_INTERNET
195113
}
196114
}
197115
}
198116

199117
fun markAsPurchased(context: Context) {
200118
val editor = getSharedPreferences(context).edit()
201-
editor.remove(KEY_ENCRYPTED_TRIAL_UTC_END_TIME)
202-
editor.remove(KEY_ENCRYPTED_TRIAL_UTC_END_TIME + "_unencrypted_fallback")
203-
editor.remove(KEY_ENCRYPTION_IV)
119+
// Remove old encryption keys if they exist, and the new unencrypted key
120+
// editor.remove("encryptedTrialUtcEndTime") // Key name from previous versions if needed for cleanup
121+
// editor.remove("encryptionIv") // Key name from previous versions if needed for cleanup
122+
// editor.remove("encryptedTrialUtcEndTime_unencrypted_fallback") // Key name from previous versions
123+
editor.remove(KEY_TRIAL_END_TIME_UNENCRYPTED)
204124
editor.remove(KEY_TRIAL_AWAITING_FIRST_INTERNET_TIME)
205125
editor.putBoolean(KEY_PURCHASED_FLAG, true)
206126
editor.apply()
207127

128+
// Keystore cleanup is not strictly necessary if the key wasn't used for this unencrypted version,
129+
// but good practice if we want to ensure no old trial keys remain.
130+
// However, to minimize changes, we will skip Keystore interactions for this diagnostic step.
131+
/*
208132
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
209133
try {
210-
val keyStore = getKeyStore()
211-
if (keyStore.containsAlias(KEY_ALIAS_TRIAL_END_TIME_KEY)) {
212-
keyStore.deleteEntry(KEY_ALIAS_TRIAL_END_TIME_KEY)
134+
val keyStore = KeyStore.getInstance("AndroidKeyStore")
135+
keyStore.load(null)
136+
if (keyStore.containsAlias("TrialEndTimeEncryptionKeyAlias")) {
137+
keyStore.deleteEntry("TrialEndTimeEncryptionKeyAlias")
213138
Log.d(TAG, "Trial encryption key deleted from KeyStore.")
214139
}
215140
} catch (e: Exception) {
216141
Log.e(TAG, "Failed to delete trial encryption key from KeyStore", e)
217142
}
218143
}
219-
Log.i(TAG, "App marked as purchased. Trial data cleared.")
144+
*/
145+
Log.i(TAG, "App marked as purchased. Trial data (including unencrypted end time) cleared.")
220146
}
221147

222148
private fun isPurchased(context: Context): Boolean {
@@ -225,13 +151,16 @@ object TrialManager {
225151

226152
fun initializeTrialStateFlagsIfNecessary(context: Context) {
227153
val prefs = getSharedPreferences(context)
154+
// Check if any trial-related flags or the new unencrypted end time key exist.
155+
// If none exist, it's likely a fresh install or data cleared state.
228156
if (!prefs.contains(KEY_TRIAL_AWAITING_FIRST_INTERNET_TIME) &&
229157
!prefs.contains(KEY_PURCHASED_FLAG) &&
230-
!prefs.contains(KEY_ENCRYPTED_TRIAL_UTC_END_TIME) &&
231-
!prefs.contains(KEY_ENCRYPTED_TRIAL_UTC_END_TIME + "_unencrypted_fallback")
158+
!prefs.contains(KEY_TRIAL_END_TIME_UNENCRYPTED) // Check for the new unencrypted key
159+
// !prefs.contains("encryptedTrialUtcEndTime") && // Check for old keys if comprehensive cleanup is desired
160+
// !prefs.contains("encryptedTrialUtcEndTime_unencrypted_fallback")
232161
) {
233162
prefs.edit().putBoolean(KEY_TRIAL_AWAITING_FIRST_INTERNET_TIME, true).apply()
234-
Log.d(TAG, "Initialized KEY_TRIAL_AWAITING_FIRST_INTERNET_TIME to true for a fresh state.")
163+
Log.d(TAG, "Initialized KEY_TRIAL_AWAITING_FIRST_INTERNET_TIME to true for a fresh state (unencrypted storage)." )
235164
}
236165
}
237166
}

0 commit comments

Comments
 (0)