@@ -3,139 +3,63 @@ package com.google.ai.sample
33import android.content.Context
44import android.content.SharedPreferences
55import android.os.Build
6- import android.security.keystore.KeyGenParameterSpec
7- import android.security.keystore.KeyProperties
8- import android.util.Base64
96import 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
178object 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