@rejourneyco/react-native 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle.kts +135 -0
- package/android/consumer-rules.pro +10 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +15 -0
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +2981 -0
- package/android/src/main/java/com/rejourney/capture/ANRHandler.kt +206 -0
- package/android/src/main/java/com/rejourney/capture/ActivityTracker.kt +98 -0
- package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +1553 -0
- package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +375 -0
- package/android/src/main/java/com/rejourney/capture/CrashHandler.kt +153 -0
- package/android/src/main/java/com/rejourney/capture/MotionEvent.kt +215 -0
- package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +512 -0
- package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +773 -0
- package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +633 -0
- package/android/src/main/java/com/rejourney/capture/ViewSerializer.kt +286 -0
- package/android/src/main/java/com/rejourney/core/Constants.kt +117 -0
- package/android/src/main/java/com/rejourney/core/Logger.kt +93 -0
- package/android/src/main/java/com/rejourney/core/Types.kt +124 -0
- package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +162 -0
- package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +747 -0
- package/android/src/main/java/com/rejourney/network/HttpClientProvider.kt +16 -0
- package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +272 -0
- package/android/src/main/java/com/rejourney/network/UploadManager.kt +1363 -0
- package/android/src/main/java/com/rejourney/network/UploadWorker.kt +492 -0
- package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +645 -0
- package/android/src/main/java/com/rejourney/touch/GestureClassifier.kt +233 -0
- package/android/src/main/java/com/rejourney/touch/KeyboardTracker.kt +158 -0
- package/android/src/main/java/com/rejourney/touch/TextInputTracker.kt +181 -0
- package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +591 -0
- package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +284 -0
- package/android/src/main/java/com/rejourney/utils/OEMDetector.kt +154 -0
- package/android/src/main/java/com/rejourney/utils/PerfTiming.kt +235 -0
- package/android/src/main/java/com/rejourney/utils/Telemetry.kt +297 -0
- package/android/src/main/java/com/rejourney/utils/WindowUtils.kt +84 -0
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +187 -0
- package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +218 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
- package/ios/Capture/RJANRHandler.h +42 -0
- package/ios/Capture/RJANRHandler.m +328 -0
- package/ios/Capture/RJCaptureEngine.h +275 -0
- package/ios/Capture/RJCaptureEngine.m +2062 -0
- package/ios/Capture/RJCaptureHeuristics.h +80 -0
- package/ios/Capture/RJCaptureHeuristics.m +903 -0
- package/ios/Capture/RJCrashHandler.h +46 -0
- package/ios/Capture/RJCrashHandler.m +313 -0
- package/ios/Capture/RJMotionEvent.h +183 -0
- package/ios/Capture/RJMotionEvent.m +183 -0
- package/ios/Capture/RJPerformanceManager.h +100 -0
- package/ios/Capture/RJPerformanceManager.m +373 -0
- package/ios/Capture/RJPixelBufferDownscaler.h +42 -0
- package/ios/Capture/RJPixelBufferDownscaler.m +85 -0
- package/ios/Capture/RJSegmentUploader.h +146 -0
- package/ios/Capture/RJSegmentUploader.m +778 -0
- package/ios/Capture/RJVideoEncoder.h +247 -0
- package/ios/Capture/RJVideoEncoder.m +1036 -0
- package/ios/Capture/RJViewControllerTracker.h +73 -0
- package/ios/Capture/RJViewControllerTracker.m +508 -0
- package/ios/Capture/RJViewHierarchyScanner.h +215 -0
- package/ios/Capture/RJViewHierarchyScanner.m +1464 -0
- package/ios/Capture/RJViewSerializer.h +119 -0
- package/ios/Capture/RJViewSerializer.m +498 -0
- package/ios/Core/RJConstants.h +124 -0
- package/ios/Core/RJConstants.m +88 -0
- package/ios/Core/RJLifecycleManager.h +85 -0
- package/ios/Core/RJLifecycleManager.m +308 -0
- package/ios/Core/RJLogger.h +61 -0
- package/ios/Core/RJLogger.m +211 -0
- package/ios/Core/RJTypes.h +176 -0
- package/ios/Core/RJTypes.m +66 -0
- package/ios/Core/Rejourney.h +64 -0
- package/ios/Core/Rejourney.mm +2495 -0
- package/ios/Network/RJDeviceAuthManager.h +94 -0
- package/ios/Network/RJDeviceAuthManager.m +967 -0
- package/ios/Network/RJNetworkMonitor.h +68 -0
- package/ios/Network/RJNetworkMonitor.m +267 -0
- package/ios/Network/RJRetryManager.h +73 -0
- package/ios/Network/RJRetryManager.m +325 -0
- package/ios/Network/RJUploadManager.h +267 -0
- package/ios/Network/RJUploadManager.m +2296 -0
- package/ios/Privacy/RJPrivacyMask.h +163 -0
- package/ios/Privacy/RJPrivacyMask.m +922 -0
- package/ios/Rejourney.h +63 -0
- package/ios/Touch/RJGestureClassifier.h +130 -0
- package/ios/Touch/RJGestureClassifier.m +333 -0
- package/ios/Touch/RJTouchInterceptor.h +169 -0
- package/ios/Touch/RJTouchInterceptor.m +772 -0
- package/ios/Utils/RJEventBuffer.h +112 -0
- package/ios/Utils/RJEventBuffer.m +358 -0
- package/ios/Utils/RJGzipUtils.h +33 -0
- package/ios/Utils/RJGzipUtils.m +89 -0
- package/ios/Utils/RJKeychainManager.h +48 -0
- package/ios/Utils/RJKeychainManager.m +111 -0
- package/ios/Utils/RJPerfTiming.h +209 -0
- package/ios/Utils/RJPerfTiming.m +264 -0
- package/ios/Utils/RJTelemetry.h +92 -0
- package/ios/Utils/RJTelemetry.m +320 -0
- package/ios/Utils/RJWindowUtils.h +66 -0
- package/ios/Utils/RJWindowUtils.m +133 -0
- package/lib/commonjs/NativeRejourney.js +40 -0
- package/lib/commonjs/components/Mask.js +79 -0
- package/lib/commonjs/index.js +1381 -0
- package/lib/commonjs/sdk/autoTracking.js +1259 -0
- package/lib/commonjs/sdk/constants.js +151 -0
- package/lib/commonjs/sdk/errorTracking.js +199 -0
- package/lib/commonjs/sdk/index.js +50 -0
- package/lib/commonjs/sdk/metricsTracking.js +204 -0
- package/lib/commonjs/sdk/navigation.js +151 -0
- package/lib/commonjs/sdk/networkInterceptor.js +412 -0
- package/lib/commonjs/sdk/utils.js +363 -0
- package/lib/commonjs/types/expo-router.d.js +2 -0
- package/lib/commonjs/types/index.js +2 -0
- package/lib/module/NativeRejourney.js +38 -0
- package/lib/module/components/Mask.js +72 -0
- package/lib/module/index.js +1284 -0
- package/lib/module/sdk/autoTracking.js +1233 -0
- package/lib/module/sdk/constants.js +145 -0
- package/lib/module/sdk/errorTracking.js +189 -0
- package/lib/module/sdk/index.js +12 -0
- package/lib/module/sdk/metricsTracking.js +187 -0
- package/lib/module/sdk/navigation.js +143 -0
- package/lib/module/sdk/networkInterceptor.js +401 -0
- package/lib/module/sdk/utils.js +342 -0
- package/lib/module/types/expo-router.d.js +2 -0
- package/lib/module/types/index.js +2 -0
- package/lib/typescript/NativeRejourney.d.ts +147 -0
- package/lib/typescript/components/Mask.d.ts +39 -0
- package/lib/typescript/index.d.ts +117 -0
- package/lib/typescript/sdk/autoTracking.d.ts +204 -0
- package/lib/typescript/sdk/constants.d.ts +120 -0
- package/lib/typescript/sdk/errorTracking.d.ts +32 -0
- package/lib/typescript/sdk/index.d.ts +9 -0
- package/lib/typescript/sdk/metricsTracking.d.ts +58 -0
- package/lib/typescript/sdk/navigation.d.ts +33 -0
- package/lib/typescript/sdk/networkInterceptor.d.ts +47 -0
- package/lib/typescript/sdk/utils.d.ts +148 -0
- package/lib/typescript/types/index.d.ts +624 -0
- package/package.json +102 -0
- package/rejourney.podspec +21 -0
- package/src/NativeRejourney.ts +165 -0
- package/src/components/Mask.tsx +80 -0
- package/src/index.ts +1459 -0
- package/src/sdk/autoTracking.ts +1373 -0
- package/src/sdk/constants.ts +134 -0
- package/src/sdk/errorTracking.ts +231 -0
- package/src/sdk/index.ts +11 -0
- package/src/sdk/metricsTracking.ts +232 -0
- package/src/sdk/navigation.ts +157 -0
- package/src/sdk/networkInterceptor.ts +440 -0
- package/src/sdk/utils.ts +369 -0
- package/src/types/expo-router.d.ts +7 -0
- package/src/types/index.ts +739 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device authentication and token management using ECDSA keypairs.
|
|
3
|
+
* Ported from iOS RJDeviceAuthManager with proper cryptographic authentication.
|
|
4
|
+
*/
|
|
5
|
+
package com.rejourney.network
|
|
6
|
+
|
|
7
|
+
import android.content.Context
|
|
8
|
+
import android.content.SharedPreferences
|
|
9
|
+
import android.os.Build
|
|
10
|
+
import android.security.keystore.KeyGenParameterSpec
|
|
11
|
+
import android.security.keystore.KeyProperties
|
|
12
|
+
import android.util.Base64
|
|
13
|
+
import androidx.security.crypto.EncryptedSharedPreferences
|
|
14
|
+
import androidx.security.crypto.MasterKey
|
|
15
|
+
import com.rejourney.core.Constants
|
|
16
|
+
import com.rejourney.core.Logger
|
|
17
|
+
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
18
|
+
import okhttp3.*
|
|
19
|
+
import okhttp3.MediaType.Companion.toMediaType
|
|
20
|
+
import okhttp3.RequestBody.Companion.toRequestBody
|
|
21
|
+
import org.json.JSONObject
|
|
22
|
+
import java.io.IOException
|
|
23
|
+
import java.security.*
|
|
24
|
+
import java.security.spec.ECGenParameterSpec
|
|
25
|
+
import java.util.concurrent.TimeUnit
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Listener interface for authentication failures.
|
|
29
|
+
* Implementations should stop recording and notify the user.
|
|
30
|
+
*/
|
|
31
|
+
interface AuthFailureListener {
|
|
32
|
+
/**
|
|
33
|
+
* Called when authentication fails due to security errors (403/404).
|
|
34
|
+
* @param errorCode HTTP status code (403 = package name mismatch, 404 = project not found)
|
|
35
|
+
* @param errorMessage Human-readable error message
|
|
36
|
+
* @param domain Error domain for categorization
|
|
37
|
+
*/
|
|
38
|
+
fun onAuthenticationFailure(errorCode: Int, errorMessage: String, domain: String)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class DeviceAuthManager private constructor(private val context: Context) {
|
|
42
|
+
|
|
43
|
+
companion object {
|
|
44
|
+
@Volatile
|
|
45
|
+
private var instance: DeviceAuthManager? = null
|
|
46
|
+
|
|
47
|
+
private const val PREFS_NAME = "rejourney_device_auth"
|
|
48
|
+
private const val KEY_CREDENTIAL_ID = "credential_id"
|
|
49
|
+
private const val KEY_UPLOAD_TOKEN = "upload_token"
|
|
50
|
+
private const val KEY_TOKEN_EXPIRY = "token_expiry"
|
|
51
|
+
private const val KEY_API_URL = "api_url"
|
|
52
|
+
private const val KEY_PROJECT_PUBLIC_KEY = "project_public_key"
|
|
53
|
+
private const val KEY_BUNDLE_ID = "bundle_id"
|
|
54
|
+
private const val KEY_PLATFORM = "platform"
|
|
55
|
+
private const val KEY_SDK_VERSION = "sdk_version"
|
|
56
|
+
|
|
57
|
+
private const val KEYSTORE_ALIAS = "com.rejourney.devicekey"
|
|
58
|
+
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
|
|
59
|
+
|
|
60
|
+
// Rate limiting constants for auto-registration
|
|
61
|
+
private const val AUTH_COOLDOWN_BASE_MS = 5000L // 5 second base cooldown
|
|
62
|
+
private const val AUTH_COOLDOWN_MAX_MS = 300000L // 5 minute max cooldown
|
|
63
|
+
private const val AUTH_MAX_CONSECUTIVE_FAILURES = 10
|
|
64
|
+
|
|
65
|
+
fun getInstance(context: Context): DeviceAuthManager {
|
|
66
|
+
return instance ?: synchronized(this) {
|
|
67
|
+
instance ?: DeviceAuthManager(context.applicationContext).also { instance = it }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Listener for authentication failures (set by RejourneyModuleImpl)
|
|
73
|
+
var authFailureListener: AuthFailureListener? = null
|
|
74
|
+
|
|
75
|
+
// Use shared client (SSL pinning removed)
|
|
76
|
+
private val client = HttpClientProvider.shared
|
|
77
|
+
|
|
78
|
+
private val prefs: SharedPreferences by lazy {
|
|
79
|
+
try {
|
|
80
|
+
val masterKey = MasterKey.Builder(context)
|
|
81
|
+
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
|
82
|
+
.build()
|
|
83
|
+
|
|
84
|
+
EncryptedSharedPreferences.create(
|
|
85
|
+
context,
|
|
86
|
+
PREFS_NAME,
|
|
87
|
+
masterKey,
|
|
88
|
+
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
89
|
+
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
90
|
+
)
|
|
91
|
+
} catch (e: Exception) {
|
|
92
|
+
Logger.warning("Failed to create encrypted prefs, using standard prefs: ${e.message}")
|
|
93
|
+
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
init {
|
|
98
|
+
loadStoredRegistrationInfo()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private var apiUrl: String = ""
|
|
102
|
+
private var projectPublicKey: String = ""
|
|
103
|
+
private var storedBundleId: String = ""
|
|
104
|
+
private var storedPlatform: String = ""
|
|
105
|
+
private var storedSdkVersion: String = ""
|
|
106
|
+
|
|
107
|
+
// Rate limiting for auto-registration
|
|
108
|
+
@Volatile private var lastFailedRegistrationTime: Long = 0
|
|
109
|
+
@Volatile private var consecutiveFailures: Int = 0
|
|
110
|
+
@Volatile private var registrationInProgress: Boolean = false
|
|
111
|
+
private val pendingTokenCallbacks = mutableListOf<(Boolean, String?, Int, String?) -> Unit>()
|
|
112
|
+
|
|
113
|
+
private fun loadStoredRegistrationInfo() {
|
|
114
|
+
apiUrl = prefs.getString(KEY_API_URL, "")?.takeIf { it.startsWith("http") } ?: ""
|
|
115
|
+
projectPublicKey = prefs.getString(KEY_PROJECT_PUBLIC_KEY, "") ?: ""
|
|
116
|
+
storedBundleId = prefs.getString(KEY_BUNDLE_ID, "") ?: ""
|
|
117
|
+
storedPlatform = prefs.getString(KEY_PLATFORM, "") ?: ""
|
|
118
|
+
storedSdkVersion = prefs.getString(KEY_SDK_VERSION, "") ?: ""
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Register device with the dashboard server.
|
|
123
|
+
* Generates ECDSA P-256 keypair if needed and stores in Android Keystore.
|
|
124
|
+
*/
|
|
125
|
+
fun registerDevice(
|
|
126
|
+
projectKey: String,
|
|
127
|
+
bundleId: String,
|
|
128
|
+
platform: String,
|
|
129
|
+
sdkVersion: String,
|
|
130
|
+
apiUrl: String,
|
|
131
|
+
callback: (success: Boolean, credentialId: String?, error: String?) -> Unit
|
|
132
|
+
) {
|
|
133
|
+
this.apiUrl = apiUrl
|
|
134
|
+
this.projectPublicKey = projectKey
|
|
135
|
+
this.storedBundleId = bundleId
|
|
136
|
+
this.storedPlatform = platform
|
|
137
|
+
this.storedSdkVersion = sdkVersion
|
|
138
|
+
prefs.edit()
|
|
139
|
+
.putString(KEY_API_URL, apiUrl)
|
|
140
|
+
.putString(KEY_PROJECT_PUBLIC_KEY, projectKey)
|
|
141
|
+
.putString(KEY_BUNDLE_ID, bundleId)
|
|
142
|
+
.putString(KEY_PLATFORM, platform)
|
|
143
|
+
.putString(KEY_SDK_VERSION, sdkVersion)
|
|
144
|
+
.apply()
|
|
145
|
+
|
|
146
|
+
// Check if already registered
|
|
147
|
+
val existingCredentialId = prefs.getString(KEY_CREDENTIAL_ID, null)
|
|
148
|
+
if (!existingCredentialId.isNullOrEmpty()) {
|
|
149
|
+
Logger.debug("Device already registered with credential: $existingCredentialId")
|
|
150
|
+
callback(true, existingCredentialId, null)
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Generate or load ECDSA keypair
|
|
155
|
+
val publicKeyPEM = try {
|
|
156
|
+
getOrCreatePublicKeyPEM()
|
|
157
|
+
} catch (e: Exception) {
|
|
158
|
+
Logger.error("Failed to generate ECDSA keypair", e)
|
|
159
|
+
callback(false, null, "Failed to generate keypair: ${e.message}")
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (publicKeyPEM == null) {
|
|
164
|
+
callback(false, null, "Failed to export public key")
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Register with backend
|
|
169
|
+
val requestBody = JSONObject().apply {
|
|
170
|
+
put("projectPublicKey", projectKey)
|
|
171
|
+
put("bundleId", bundleId)
|
|
172
|
+
put("platform", platform)
|
|
173
|
+
put("sdkVersion", sdkVersion)
|
|
174
|
+
put("devicePublicKey", publicKeyPEM)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
Logger.debug("Registering device with backend...")
|
|
178
|
+
|
|
179
|
+
val request = Request.Builder()
|
|
180
|
+
.url("$apiUrl/api/devices/register")
|
|
181
|
+
.post(requestBody.toString().toRequestBody("application/json".toMediaType()))
|
|
182
|
+
.build()
|
|
183
|
+
|
|
184
|
+
client.newCall(request).enqueue(object : Callback {
|
|
185
|
+
override fun onFailure(call: Call, e: IOException) {
|
|
186
|
+
Logger.error("Device registration failed", e)
|
|
187
|
+
callback(false, null, e.message)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
override fun onResponse(call: Call, response: Response) {
|
|
191
|
+
response.use {
|
|
192
|
+
if (it.isSuccessful) {
|
|
193
|
+
try {
|
|
194
|
+
val json = JSONObject(it.body?.string() ?: "{}")
|
|
195
|
+
val credentialId = json.optString("deviceCredentialId")
|
|
196
|
+
|
|
197
|
+
prefs.edit().putString(KEY_CREDENTIAL_ID, credentialId).apply()
|
|
198
|
+
|
|
199
|
+
Logger.debug("Device registered: $credentialId")
|
|
200
|
+
callback(true, credentialId, null)
|
|
201
|
+
} catch (e: Exception) {
|
|
202
|
+
Logger.error("Failed to parse registration response", e)
|
|
203
|
+
callback(false, null, "Failed to parse response")
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
val errorBody = it.body?.string() ?: ""
|
|
207
|
+
val errorCode = it.code
|
|
208
|
+
Logger.error("Registration failed: $errorCode - $errorBody")
|
|
209
|
+
|
|
210
|
+
when (errorCode) {
|
|
211
|
+
403 -> {
|
|
212
|
+
// Package name mismatch or access forbidden
|
|
213
|
+
Logger.error("SECURITY: Package name mismatch or access forbidden")
|
|
214
|
+
val errorMessage = parseErrorMessage(errorBody)
|
|
215
|
+
?: "Package name mismatch. The app package name does not match the project configuration."
|
|
216
|
+
clearCredentials()
|
|
217
|
+
authFailureListener?.onAuthenticationFailure(403, errorMessage, "RJDeviceAuth")
|
|
218
|
+
callback(false, null, errorMessage)
|
|
219
|
+
}
|
|
220
|
+
404 -> {
|
|
221
|
+
// Project not found - invalid project key
|
|
222
|
+
Logger.error("SECURITY: Project not found. Invalid project key.")
|
|
223
|
+
val errorMessage = parseErrorMessage(errorBody) ?: "Project not found. Invalid project key."
|
|
224
|
+
clearCredentials()
|
|
225
|
+
authFailureListener?.onAuthenticationFailure(404, errorMessage, "RJDeviceAuth")
|
|
226
|
+
callback(false, null, errorMessage)
|
|
227
|
+
}
|
|
228
|
+
else -> {
|
|
229
|
+
callback(false, null, "Registration failed: $errorCode")
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get upload token via challenge-response authentication.
|
|
240
|
+
*/
|
|
241
|
+
fun getUploadToken(
|
|
242
|
+
callback: (success: Boolean, token: String?, expiresIn: Int, error: String?) -> Unit
|
|
243
|
+
) {
|
|
244
|
+
// Check cached token
|
|
245
|
+
val cachedToken = prefs.getString(KEY_UPLOAD_TOKEN, null)
|
|
246
|
+
val tokenExpiry = prefs.getLong(KEY_TOKEN_EXPIRY, 0)
|
|
247
|
+
|
|
248
|
+
if (!cachedToken.isNullOrEmpty() && tokenExpiry > System.currentTimeMillis() + 60_000) {
|
|
249
|
+
val remainingSeconds = ((tokenExpiry - System.currentTimeMillis()) / 1000).toInt()
|
|
250
|
+
Logger.debug("Using cached upload token (expires in $remainingSeconds seconds)")
|
|
251
|
+
callback(true, cachedToken, remainingSeconds, null)
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
val credentialId = prefs.getString(KEY_CREDENTIAL_ID, null)
|
|
256
|
+
if (credentialId.isNullOrEmpty()) {
|
|
257
|
+
Logger.warning("Cannot get upload token: device not registered")
|
|
258
|
+
callback(false, null, 0, "Device not registered")
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
val savedApiUrl = prefs.getString(KEY_API_URL, null)?.takeIf { it.startsWith("http") } ?: "https://api.rejourney.co"
|
|
263
|
+
|
|
264
|
+
// Step 1: Request challenge
|
|
265
|
+
requestChallenge(credentialId) { challengeSuccess, challenge, nonce, challengeError ->
|
|
266
|
+
if (!challengeSuccess || challenge == null || nonce == null) {
|
|
267
|
+
callback(false, null, 0, challengeError ?: "Failed to get challenge")
|
|
268
|
+
return@requestChallenge
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Step 2: Sign challenge
|
|
272
|
+
val signature = try {
|
|
273
|
+
signChallenge(challenge)
|
|
274
|
+
} catch (e: Exception) {
|
|
275
|
+
Logger.error("Failed to sign challenge", e)
|
|
276
|
+
callback(false, null, 0, "Failed to sign challenge: ${e.message}")
|
|
277
|
+
return@requestChallenge
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (signature == null) {
|
|
281
|
+
callback(false, null, 0, "Failed to sign challenge")
|
|
282
|
+
return@requestChallenge
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Step 3: Start session with signed challenge
|
|
286
|
+
startSession(credentialId, challenge, nonce, signature, callback)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get current upload token if valid.
|
|
292
|
+
*/
|
|
293
|
+
fun getCurrentUploadToken(): String? {
|
|
294
|
+
val token = prefs.getString(KEY_UPLOAD_TOKEN, null)
|
|
295
|
+
val expiry = prefs.getLong(KEY_TOKEN_EXPIRY, 0)
|
|
296
|
+
|
|
297
|
+
return if (!token.isNullOrEmpty() && expiry > System.currentTimeMillis()) {
|
|
298
|
+
token
|
|
299
|
+
} else {
|
|
300
|
+
null
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Suspend function to refresh upload token for coroutine callers.
|
|
306
|
+
*/
|
|
307
|
+
@OptIn(ExperimentalCoroutinesApi::class)
|
|
308
|
+
suspend fun refreshUploadToken(): Boolean {
|
|
309
|
+
return kotlinx.coroutines.suspendCancellableCoroutine { continuation ->
|
|
310
|
+
getUploadToken { success, _, _, _ ->
|
|
311
|
+
if (continuation.isActive) {
|
|
312
|
+
continuation.resume(success, onCancellation = { _ -> })
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Check if upload token is still valid.
|
|
320
|
+
*/
|
|
321
|
+
fun hasValidUploadToken(): Boolean {
|
|
322
|
+
val token = prefs.getString(KEY_UPLOAD_TOKEN, null)
|
|
323
|
+
val expiry = prefs.getLong(KEY_TOKEN_EXPIRY, 0)
|
|
324
|
+
val isValid = !token.isNullOrEmpty() && expiry > System.currentTimeMillis() + 60_000
|
|
325
|
+
Logger.debug("[DeviceAuthManager] hasValidUploadToken: $isValid (token=${if (token.isNullOrEmpty()) "null" else "present"}, expiresIn=${(expiry - System.currentTimeMillis()) / 1000}s)")
|
|
326
|
+
return isValid
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private fun canAutoRegister(): Boolean {
|
|
330
|
+
if (apiUrl.isBlank() || projectPublicKey.isBlank() || storedBundleId.isBlank() ||
|
|
331
|
+
storedPlatform.isBlank() || storedSdkVersion.isBlank()) {
|
|
332
|
+
loadStoredRegistrationInfo()
|
|
333
|
+
}
|
|
334
|
+
return apiUrl.isNotBlank() && projectPublicKey.isNotBlank() &&
|
|
335
|
+
storedBundleId.isNotBlank() && storedPlatform.isNotBlank() &&
|
|
336
|
+
storedSdkVersion.isNotBlank()
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private fun drainPendingCallbacks(
|
|
340
|
+
success: Boolean,
|
|
341
|
+
token: String?,
|
|
342
|
+
expiresIn: Int,
|
|
343
|
+
error: String?
|
|
344
|
+
) {
|
|
345
|
+
val callbacks = synchronized(pendingTokenCallbacks) {
|
|
346
|
+
val copy = pendingTokenCallbacks.toList()
|
|
347
|
+
pendingTokenCallbacks.clear()
|
|
348
|
+
registrationInProgress = false
|
|
349
|
+
copy
|
|
350
|
+
}
|
|
351
|
+
callbacks.forEach { it(success, token, expiresIn, error) }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
fun getUploadTokenWithAutoRegister(
|
|
355
|
+
callback: (success: Boolean, token: String?, expiresIn: Int, error: String?) -> Unit
|
|
356
|
+
) {
|
|
357
|
+
if (hasValidUploadToken()) {
|
|
358
|
+
val token = prefs.getString(KEY_UPLOAD_TOKEN, null)
|
|
359
|
+
val expiry = prefs.getLong(KEY_TOKEN_EXPIRY, 0)
|
|
360
|
+
val remainingSeconds = ((expiry - System.currentTimeMillis()) / 1000).toInt()
|
|
361
|
+
callback(true, token, remainingSeconds, null)
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
val credentialId = prefs.getString(KEY_CREDENTIAL_ID, null)
|
|
366
|
+
if (!credentialId.isNullOrEmpty()) {
|
|
367
|
+
getUploadToken(callback)
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (!canAutoRegister()) {
|
|
372
|
+
callback(false, null, 0, "Device not registered and auto-registration not configured")
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (consecutiveFailures >= AUTH_MAX_CONSECUTIVE_FAILURES) {
|
|
377
|
+
callback(false, null, 0, "Auto-registration disabled after repeated failures")
|
|
378
|
+
return
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
val now = System.currentTimeMillis()
|
|
382
|
+
if (consecutiveFailures > 0 && lastFailedRegistrationTime > 0) {
|
|
383
|
+
val cooldown = minOf(
|
|
384
|
+
AUTH_COOLDOWN_BASE_MS * (1L shl (consecutiveFailures - 1)),
|
|
385
|
+
AUTH_COOLDOWN_MAX_MS
|
|
386
|
+
)
|
|
387
|
+
val timeSinceFailure = now - lastFailedRegistrationTime
|
|
388
|
+
if (timeSinceFailure < cooldown) {
|
|
389
|
+
val remaining = (cooldown - timeSinceFailure) / 1000
|
|
390
|
+
callback(false, null, 0, "Rate limited - retry in ${remaining}s")
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
synchronized(pendingTokenCallbacks) {
|
|
396
|
+
pendingTokenCallbacks.add(callback)
|
|
397
|
+
if (registrationInProgress) {
|
|
398
|
+
Logger.debug("[DeviceAuthManager] Auto-registration already in progress, callback queued")
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
registrationInProgress = true
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
registerDevice(
|
|
405
|
+
projectKey = projectPublicKey,
|
|
406
|
+
bundleId = storedBundleId,
|
|
407
|
+
platform = storedPlatform,
|
|
408
|
+
sdkVersion = storedSdkVersion,
|
|
409
|
+
apiUrl = this.apiUrl
|
|
410
|
+
) { success, _, error ->
|
|
411
|
+
if (!success) {
|
|
412
|
+
consecutiveFailures++
|
|
413
|
+
lastFailedRegistrationTime = System.currentTimeMillis()
|
|
414
|
+
Logger.warning("[DeviceAuthManager] Auto-registration failed: $error")
|
|
415
|
+
drainPendingCallbacks(false, null, 0, error ?: "Registration failed")
|
|
416
|
+
return@registerDevice
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
consecutiveFailures = 0
|
|
420
|
+
lastFailedRegistrationTime = 0
|
|
421
|
+
|
|
422
|
+
getUploadToken { tokenSuccess, token, expiresIn, tokenError ->
|
|
423
|
+
drainPendingCallbacks(tokenSuccess, token, expiresIn, tokenError)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Ensure a valid upload token exists before proceeding with uploads.
|
|
430
|
+
* This is the critical fix - blocks until token is refreshed if expired/missing.
|
|
431
|
+
* Returns true if token is valid after this call, false if refresh failed.
|
|
432
|
+
*/
|
|
433
|
+
@OptIn(ExperimentalCoroutinesApi::class)
|
|
434
|
+
suspend fun ensureValidToken(): Boolean {
|
|
435
|
+
Logger.debug("[DeviceAuthManager] ensureValidToken() called")
|
|
436
|
+
|
|
437
|
+
// Always refresh from SharedPreferences before network operations
|
|
438
|
+
loadStoredRegistrationInfo()
|
|
439
|
+
|
|
440
|
+
return kotlinx.coroutines.suspendCancellableCoroutine { continuation ->
|
|
441
|
+
getUploadTokenWithAutoRegister { success, token, expiresIn, error ->
|
|
442
|
+
if (continuation.isActive) {
|
|
443
|
+
if (success && !token.isNullOrEmpty()) {
|
|
444
|
+
Logger.debug("[DeviceAuthManager] Token refresh SUCCESS (expiresIn=${expiresIn}s)")
|
|
445
|
+
continuation.resume(true, onCancellation = { _ -> })
|
|
446
|
+
} else {
|
|
447
|
+
Logger.error("[DeviceAuthManager] Token refresh FAILED: $error")
|
|
448
|
+
continuation.resume(false, onCancellation = { _ -> })
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Get stored project public key (for recovery uploads).
|
|
457
|
+
*/
|
|
458
|
+
fun getCurrentPublicKey(): String? = prefs.getString(KEY_PROJECT_PUBLIC_KEY, null)
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Get stored API URL (for recovery uploads).
|
|
462
|
+
*/
|
|
463
|
+
fun getCurrentApiUrl(): String? = prefs.getString(KEY_API_URL, null)
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Get stored device hash (for recovery uploads).
|
|
467
|
+
* This is derived from the credential ID.
|
|
468
|
+
*/
|
|
469
|
+
fun getCurrentDeviceHash(): String? = prefs.getString(KEY_CREDENTIAL_ID, null)
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Clear stored credentials.
|
|
473
|
+
*/
|
|
474
|
+
fun clearCredentials() {
|
|
475
|
+
prefs.edit()
|
|
476
|
+
.remove(KEY_CREDENTIAL_ID)
|
|
477
|
+
.remove(KEY_UPLOAD_TOKEN)
|
|
478
|
+
.remove(KEY_TOKEN_EXPIRY)
|
|
479
|
+
.apply()
|
|
480
|
+
|
|
481
|
+
// Also delete the private key from Keystore
|
|
482
|
+
try {
|
|
483
|
+
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
|
|
484
|
+
keyStore.load(null)
|
|
485
|
+
keyStore.deleteEntry(KEYSTORE_ALIAS)
|
|
486
|
+
Logger.debug("Cleared all device auth data")
|
|
487
|
+
} catch (e: Exception) {
|
|
488
|
+
Logger.warning("Failed to delete private key: ${e.message}")
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Parse error message from JSON response body.
|
|
494
|
+
*/
|
|
495
|
+
private fun parseErrorMessage(responseBody: String): String? {
|
|
496
|
+
return try {
|
|
497
|
+
val json = JSONObject(responseBody)
|
|
498
|
+
val message = json.optString("message", "")
|
|
499
|
+
if (message.isNotBlank()) {
|
|
500
|
+
message
|
|
501
|
+
} else {
|
|
502
|
+
json.optString("error", "").ifBlank { null }
|
|
503
|
+
}
|
|
504
|
+
} catch (e: Exception) {
|
|
505
|
+
null
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ==================== ECDSA Keypair Management ====================
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Get or create ECDSA P-256 keypair and return public key in PEM format.
|
|
513
|
+
*/
|
|
514
|
+
private fun getOrCreatePublicKeyPEM(): String? {
|
|
515
|
+
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
|
|
516
|
+
keyStore.load(null)
|
|
517
|
+
|
|
518
|
+
// Try to load existing key
|
|
519
|
+
if (keyStore.containsAlias(KEYSTORE_ALIAS)) {
|
|
520
|
+
Logger.debug("Loaded existing ECDSA private key")
|
|
521
|
+
val certificate = keyStore.getCertificate(KEYSTORE_ALIAS)
|
|
522
|
+
return exportPublicKeyToPEM(certificate.publicKey)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Generate new keypair
|
|
526
|
+
Logger.debug("Generating new ECDSA P-256 keypair")
|
|
527
|
+
|
|
528
|
+
val keyPairGenerator = KeyPairGenerator.getInstance(
|
|
529
|
+
KeyProperties.KEY_ALGORITHM_EC,
|
|
530
|
+
ANDROID_KEYSTORE
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
val parameterSpec = KeyGenParameterSpec.Builder(
|
|
534
|
+
KEYSTORE_ALIAS,
|
|
535
|
+
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
|
|
536
|
+
)
|
|
537
|
+
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) // P-256
|
|
538
|
+
.setDigests(KeyProperties.DIGEST_SHA256)
|
|
539
|
+
.setUserAuthenticationRequired(false) // No biometric required
|
|
540
|
+
.build()
|
|
541
|
+
|
|
542
|
+
keyPairGenerator.initialize(parameterSpec)
|
|
543
|
+
val keyPair = keyPairGenerator.generateKeyPair()
|
|
544
|
+
|
|
545
|
+
Logger.debug("Successfully generated ECDSA P-256 keypair")
|
|
546
|
+
return exportPublicKeyToPEM(keyPair.public)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Export public key to PEM format.
|
|
551
|
+
*/
|
|
552
|
+
private fun exportPublicKeyToPEM(publicKey: PublicKey): String? {
|
|
553
|
+
return try {
|
|
554
|
+
// Get the raw public key bytes (X.509 SubjectPublicKeyInfo format)
|
|
555
|
+
val encoded = publicKey.encoded
|
|
556
|
+
val base64 = Base64.encodeToString(encoded, Base64.NO_WRAP)
|
|
557
|
+
|
|
558
|
+
// Wrap in PEM format
|
|
559
|
+
"-----BEGIN PUBLIC KEY-----\n$base64\n-----END PUBLIC KEY-----"
|
|
560
|
+
} catch (e: Exception) {
|
|
561
|
+
Logger.error("Failed to export public key", e)
|
|
562
|
+
null
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Sign a challenge using the private key.
|
|
568
|
+
*/
|
|
569
|
+
private fun signChallenge(challenge: String): String? {
|
|
570
|
+
return try {
|
|
571
|
+
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
|
|
572
|
+
keyStore.load(null)
|
|
573
|
+
|
|
574
|
+
val privateKey = keyStore.getKey(KEYSTORE_ALIAS, null) as? PrivateKey
|
|
575
|
+
?: throw Exception("Private key not found")
|
|
576
|
+
|
|
577
|
+
// Decode base64 challenge
|
|
578
|
+
val challengeBytes = Base64.decode(challenge, Base64.DEFAULT)
|
|
579
|
+
|
|
580
|
+
// Sign with ECDSA-SHA256
|
|
581
|
+
val signature = Signature.getInstance("SHA256withECDSA")
|
|
582
|
+
signature.initSign(privateKey)
|
|
583
|
+
signature.update(challengeBytes)
|
|
584
|
+
val signatureBytes = signature.sign()
|
|
585
|
+
|
|
586
|
+
// Return base64-encoded signature
|
|
587
|
+
Base64.encodeToString(signatureBytes, Base64.NO_WRAP)
|
|
588
|
+
} catch (e: Exception) {
|
|
589
|
+
Logger.error("Failed to sign challenge", e)
|
|
590
|
+
null
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ==================== Backend Communication ====================
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Request challenge from backend.
|
|
598
|
+
*/
|
|
599
|
+
private fun requestChallenge(
|
|
600
|
+
credentialId: String,
|
|
601
|
+
callback: (success: Boolean, challenge: String?, nonce: String?, error: String?) -> Unit
|
|
602
|
+
) {
|
|
603
|
+
val requestBody = JSONObject().apply {
|
|
604
|
+
put("deviceCredentialId", credentialId)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
Logger.debug("Requesting challenge from backend...")
|
|
608
|
+
|
|
609
|
+
val request = Request.Builder()
|
|
610
|
+
.url("$apiUrl/api/devices/challenge")
|
|
611
|
+
.post(requestBody.toString().toRequestBody("application/json".toMediaType()))
|
|
612
|
+
.build()
|
|
613
|
+
|
|
614
|
+
client.newCall(request).enqueue(object : Callback {
|
|
615
|
+
override fun onFailure(call: Call, e: IOException) {
|
|
616
|
+
Logger.error("Challenge request failed", e)
|
|
617
|
+
callback(false, null, null, e.message)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
override fun onResponse(call: Call, response: Response) {
|
|
621
|
+
response.use {
|
|
622
|
+
if (it.isSuccessful) {
|
|
623
|
+
try {
|
|
624
|
+
val json = JSONObject(it.body?.string() ?: "{}")
|
|
625
|
+
val challenge = json.optString("challenge")
|
|
626
|
+
val nonce = json.optString("nonce")
|
|
627
|
+
|
|
628
|
+
Logger.debug("Received challenge from backend")
|
|
629
|
+
callback(true, challenge, nonce, null)
|
|
630
|
+
} catch (e: Exception) {
|
|
631
|
+
Logger.error("Failed to parse challenge response", e)
|
|
632
|
+
callback(false, null, null, "Failed to parse response")
|
|
633
|
+
}
|
|
634
|
+
} else {
|
|
635
|
+
val errorBody = it.body?.string() ?: ""
|
|
636
|
+
val errorCode = it.code
|
|
637
|
+
Logger.error("Challenge request failed: $errorCode - $errorBody")
|
|
638
|
+
|
|
639
|
+
when (errorCode) {
|
|
640
|
+
403 -> {
|
|
641
|
+
// Access forbidden - security error
|
|
642
|
+
Logger.error("SECURITY: Challenge request forbidden")
|
|
643
|
+
val errorMessage = parseErrorMessage(errorBody) ?: "Access forbidden"
|
|
644
|
+
clearCredentials()
|
|
645
|
+
authFailureListener?.onAuthenticationFailure(403, errorMessage, "RJDeviceAuth")
|
|
646
|
+
callback(false, null, null, errorMessage)
|
|
647
|
+
}
|
|
648
|
+
404 -> {
|
|
649
|
+
// Device credential rejected by backend (likely DB reset or invalid)
|
|
650
|
+
Logger.error("SECURITY: Device credential not found")
|
|
651
|
+
val errorMessage = parseErrorMessage(errorBody) ?: "Credential not found"
|
|
652
|
+
clearCredentials()
|
|
653
|
+
authFailureListener?.onAuthenticationFailure(404, errorMessage, "RJDeviceAuth")
|
|
654
|
+
callback(false, null, null, errorMessage)
|
|
655
|
+
}
|
|
656
|
+
else -> {
|
|
657
|
+
callback(false, null, null, "Challenge request failed: $errorCode")
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
})
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Start session with signed challenge to get upload token.
|
|
668
|
+
*/
|
|
669
|
+
private fun startSession(
|
|
670
|
+
credentialId: String,
|
|
671
|
+
challenge: String,
|
|
672
|
+
nonce: String,
|
|
673
|
+
signature: String,
|
|
674
|
+
callback: (success: Boolean, token: String?, expiresIn: Int, error: String?) -> Unit
|
|
675
|
+
) {
|
|
676
|
+
val requestBody = JSONObject().apply {
|
|
677
|
+
put("deviceCredentialId", credentialId)
|
|
678
|
+
put("challenge", challenge)
|
|
679
|
+
put("signature", signature)
|
|
680
|
+
put("nonce", nonce)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
Logger.debug("Starting session with signed challenge...")
|
|
684
|
+
|
|
685
|
+
val request = Request.Builder()
|
|
686
|
+
.url("$apiUrl/api/devices/start-session")
|
|
687
|
+
.post(requestBody.toString().toRequestBody("application/json".toMediaType()))
|
|
688
|
+
.build()
|
|
689
|
+
|
|
690
|
+
client.newCall(request).enqueue(object : Callback {
|
|
691
|
+
override fun onFailure(call: Call, e: IOException) {
|
|
692
|
+
Logger.error("Start-session request failed", e)
|
|
693
|
+
callback(false, null, 0, e.message)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
override fun onResponse(call: Call, response: Response) {
|
|
697
|
+
response.use {
|
|
698
|
+
if (it.isSuccessful) {
|
|
699
|
+
try {
|
|
700
|
+
val json = JSONObject(it.body?.string() ?: "{}")
|
|
701
|
+
val token = json.optString("uploadToken")
|
|
702
|
+
val expiresIn = json.optInt("expiresIn", 3600)
|
|
703
|
+
|
|
704
|
+
// Cache token
|
|
705
|
+
prefs.edit()
|
|
706
|
+
.putString(KEY_UPLOAD_TOKEN, token)
|
|
707
|
+
.putLong(KEY_TOKEN_EXPIRY, System.currentTimeMillis() + (expiresIn * 1000L))
|
|
708
|
+
.apply()
|
|
709
|
+
|
|
710
|
+
Logger.debug("Got upload token (expires in $expiresIn seconds)")
|
|
711
|
+
callback(true, token, expiresIn, null)
|
|
712
|
+
} catch (e: Exception) {
|
|
713
|
+
Logger.error("Failed to parse start-session response", e)
|
|
714
|
+
callback(false, null, 0, "Failed to parse response")
|
|
715
|
+
}
|
|
716
|
+
} else {
|
|
717
|
+
val errorBody = it.body?.string() ?: ""
|
|
718
|
+
val errorCode = it.code
|
|
719
|
+
Logger.error("Start-session failed: $errorCode - $errorBody")
|
|
720
|
+
|
|
721
|
+
when (errorCode) {
|
|
722
|
+
403 -> {
|
|
723
|
+
// Access forbidden - security error
|
|
724
|
+
Logger.error("SECURITY: Start-session forbidden")
|
|
725
|
+
val errorMessage = parseErrorMessage(errorBody) ?: "Access forbidden"
|
|
726
|
+
clearCredentials()
|
|
727
|
+
authFailureListener?.onAuthenticationFailure(403, errorMessage, "RJDeviceAuth")
|
|
728
|
+
callback(false, null, 0, errorMessage)
|
|
729
|
+
}
|
|
730
|
+
404 -> {
|
|
731
|
+
// Credential or project not found
|
|
732
|
+
Logger.error("SECURITY: Start-session resource not found")
|
|
733
|
+
val errorMessage = parseErrorMessage(errorBody) ?: "Resource not found"
|
|
734
|
+
clearCredentials()
|
|
735
|
+
authFailureListener?.onAuthenticationFailure(404, errorMessage, "RJDeviceAuth")
|
|
736
|
+
callback(false, null, 0, errorMessage)
|
|
737
|
+
}
|
|
738
|
+
else -> {
|
|
739
|
+
callback(false, null, 0, "Start-session failed: $errorCode")
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
})
|
|
746
|
+
}
|
|
747
|
+
}
|