@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,967 @@
|
|
|
1
|
+
//
|
|
2
|
+
// RJDeviceAuthManager.m
|
|
3
|
+
// Rejourney
|
|
4
|
+
//
|
|
5
|
+
// Device authentication using ECDSA keypairs
|
|
6
|
+
// Copyright (c) 2026 Rejourney
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
#import "RJDeviceAuthManager.h"
|
|
10
|
+
#import "../Core/RJLogger.h"
|
|
11
|
+
#import "../Utils/RJKeychainManager.h"
|
|
12
|
+
#import <Security/Security.h>
|
|
13
|
+
|
|
14
|
+
static NSString *const kKeychainKeyTag = @"com.rejourney.devicekey";
|
|
15
|
+
static NSString *const kKeychainAccessGroup = nil;
|
|
16
|
+
static NSString *const kDeviceCredentialKey = @"RJDeviceCredentialId";
|
|
17
|
+
static NSString *const kUploadTokenKey = @"RJUploadToken";
|
|
18
|
+
static NSString *const kUploadTokenExpiryKey = @"RJUploadTokenExpiry";
|
|
19
|
+
|
|
20
|
+
@interface RJDeviceAuthManager ()
|
|
21
|
+
@property(nonatomic, strong) dispatch_queue_t authQueue;
|
|
22
|
+
@property(nonatomic, copy, nullable) NSString *cachedDeviceCredentialId;
|
|
23
|
+
@property(nonatomic, copy, nullable) NSString *cachedUploadToken;
|
|
24
|
+
@property(nonatomic, strong, nullable) NSDate *uploadTokenExpiry;
|
|
25
|
+
@property(nonatomic, copy, nullable) NSString *apiUrl;
|
|
26
|
+
@property(nonatomic, copy, nullable) NSString *projectPublicKey;
|
|
27
|
+
@property(nonatomic, copy, nullable) NSString *storedBundleId;
|
|
28
|
+
@property(nonatomic, copy, nullable) NSString *storedPlatform;
|
|
29
|
+
@property(nonatomic, copy, nullable) NSString *storedSdkVersion;
|
|
30
|
+
@property(nonatomic, assign) BOOL registrationInProgress;
|
|
31
|
+
@property(nonatomic, strong, nullable) NSMutableArray<RJDeviceTokenCompletionHandler> *pendingTokenCallbacks;
|
|
32
|
+
// Cooldown to prevent flood of failed registrations
|
|
33
|
+
@property(nonatomic, assign) NSTimeInterval lastFailedRegistrationTime;
|
|
34
|
+
@property(nonatomic, assign) NSInteger consecutiveFailures;
|
|
35
|
+
@end
|
|
36
|
+
|
|
37
|
+
// Cooldown constants
|
|
38
|
+
static const NSTimeInterval RJ_AUTH_COOLDOWN_BASE_SECONDS = 5.0; // 5 second base cooldown
|
|
39
|
+
static const NSTimeInterval RJ_AUTH_COOLDOWN_MAX_SECONDS = 300.0; // 5 minute max cooldown
|
|
40
|
+
static const NSInteger RJ_AUTH_MAX_CONSECUTIVE_FAILURES = 10;
|
|
41
|
+
|
|
42
|
+
@implementation RJDeviceAuthManager
|
|
43
|
+
|
|
44
|
+
+ (instancetype)sharedManager {
|
|
45
|
+
static RJDeviceAuthManager *sharedInstance = nil;
|
|
46
|
+
static dispatch_once_t onceToken;
|
|
47
|
+
dispatch_once(&onceToken, ^{
|
|
48
|
+
sharedInstance = [[self alloc] init];
|
|
49
|
+
});
|
|
50
|
+
return sharedInstance;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
- (instancetype)init {
|
|
54
|
+
self = [super init];
|
|
55
|
+
if (self) {
|
|
56
|
+
_authQueue = dispatch_queue_create("com.rejourney.deviceauth",
|
|
57
|
+
DISPATCH_QUEUE_SERIAL);
|
|
58
|
+
_pendingTokenCallbacks = [NSMutableArray array];
|
|
59
|
+
_registrationInProgress = NO;
|
|
60
|
+
[self loadStoredCredentials];
|
|
61
|
+
}
|
|
62
|
+
return self;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#pragma mark - Public Methods
|
|
66
|
+
|
|
67
|
+
- (void)registerDeviceWithProjectKey:(NSString *)projectPublicKey
|
|
68
|
+
bundleId:(NSString *)bundleId
|
|
69
|
+
platform:(NSString *)platform
|
|
70
|
+
sdkVersion:(NSString *)sdkVersion
|
|
71
|
+
apiUrl:(NSString *)apiUrl
|
|
72
|
+
completion:(RJDeviceAuthCompletionHandler)completion {
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
self.apiUrl = apiUrl;
|
|
76
|
+
self.projectPublicKey = projectPublicKey;
|
|
77
|
+
self.storedBundleId = bundleId;
|
|
78
|
+
self.storedPlatform = platform;
|
|
79
|
+
self.storedSdkVersion = sdkVersion;
|
|
80
|
+
|
|
81
|
+
RJLogDebug(@"configured apiUrl: '%@'", apiUrl);
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if (self.cachedDeviceCredentialId) {
|
|
85
|
+
RJLogDebug(@"Device already registered with credential: %@",
|
|
86
|
+
self.cachedDeviceCredentialId);
|
|
87
|
+
if (completion) {
|
|
88
|
+
completion(YES, self.cachedDeviceCredentialId, nil);
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
dispatch_async(self.authQueue, ^{
|
|
94
|
+
@autoreleasepool {
|
|
95
|
+
|
|
96
|
+
SecKeyRef privateKey = [self getOrCreatePrivateKey];
|
|
97
|
+
if (!privateKey) {
|
|
98
|
+
NSError *error =
|
|
99
|
+
[NSError errorWithDomain:@"RJDeviceAuth"
|
|
100
|
+
code:1001
|
|
101
|
+
userInfo:@{
|
|
102
|
+
NSLocalizedDescriptionKey :
|
|
103
|
+
@"Failed to generate ECDSA keypair"
|
|
104
|
+
}];
|
|
105
|
+
RJLogError(@"Failed to generate keypair");
|
|
106
|
+
if (completion) {
|
|
107
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
108
|
+
completion(NO, nil, error);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey);
|
|
116
|
+
if (!publicKey) {
|
|
117
|
+
CFRelease(privateKey);
|
|
118
|
+
NSError *error = [NSError
|
|
119
|
+
errorWithDomain:@"RJDeviceAuth"
|
|
120
|
+
code:1002
|
|
121
|
+
userInfo:@{
|
|
122
|
+
NSLocalizedDescriptionKey : @"Failed to extract public key"
|
|
123
|
+
}];
|
|
124
|
+
RJLogError(@"Failed to extract public key");
|
|
125
|
+
if (completion) {
|
|
126
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
127
|
+
completion(NO, nil, error);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
NSString *publicKeyPEM = [self exportPublicKeyToPEM:publicKey];
|
|
134
|
+
CFRelease(publicKey);
|
|
135
|
+
CFRelease(privateKey);
|
|
136
|
+
|
|
137
|
+
if (!publicKeyPEM) {
|
|
138
|
+
NSError *error = [NSError
|
|
139
|
+
errorWithDomain:@"RJDeviceAuth"
|
|
140
|
+
code:1003
|
|
141
|
+
userInfo:@{
|
|
142
|
+
NSLocalizedDescriptionKey : @"Failed to export public key"
|
|
143
|
+
}];
|
|
144
|
+
RJLogError(@"Failed to export public key to PEM");
|
|
145
|
+
if (completion) {
|
|
146
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
147
|
+
completion(NO, nil, error);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
[self registerWithBackend:projectPublicKey
|
|
155
|
+
bundleId:bundleId
|
|
156
|
+
platform:platform
|
|
157
|
+
sdkVersion:sdkVersion
|
|
158
|
+
publicKeyPEM:publicKeyPEM
|
|
159
|
+
completion:completion];
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
- (void)getUploadTokenWithCompletion:
|
|
165
|
+
(RJDeviceTokenCompletionHandler)completion {
|
|
166
|
+
|
|
167
|
+
if ([self hasValidUploadToken]) {
|
|
168
|
+
NSTimeInterval remainingTime =
|
|
169
|
+
[self.uploadTokenExpiry timeIntervalSinceNow];
|
|
170
|
+
RJLogDebug(@"Using cached upload token (expires in %ld seconds)",
|
|
171
|
+
(long)remainingTime);
|
|
172
|
+
if (completion) {
|
|
173
|
+
completion(YES, self.cachedUploadToken, (NSInteger)remainingTime, nil);
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
dispatch_async(self.authQueue, ^{
|
|
180
|
+
@autoreleasepool {
|
|
181
|
+
if (!self.cachedDeviceCredentialId) {
|
|
182
|
+
NSError *error = [NSError
|
|
183
|
+
errorWithDomain:@"RJDeviceAuth"
|
|
184
|
+
code:2001
|
|
185
|
+
userInfo:@{
|
|
186
|
+
NSLocalizedDescriptionKey : @"Device not registered"
|
|
187
|
+
}];
|
|
188
|
+
RJLogError(@"Cannot get upload token: device not registered");
|
|
189
|
+
if (completion) {
|
|
190
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
191
|
+
completion(NO, nil, 0, error);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
[self requestChallengeWithCompletion:^(BOOL success, NSString *challenge,
|
|
199
|
+
NSString *nonce, NSError *error) {
|
|
200
|
+
if (!success || !challenge || !nonce) {
|
|
201
|
+
RJLogError(@"Failed to get challenge: %@", error);
|
|
202
|
+
if (completion) {
|
|
203
|
+
completion(NO, nil, 0, error);
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
NSString *signature = [self signChallenge:challenge];
|
|
210
|
+
if (!signature) {
|
|
211
|
+
NSError *signError = [NSError
|
|
212
|
+
errorWithDomain:@"RJDeviceAuth"
|
|
213
|
+
code:2002
|
|
214
|
+
userInfo:@{
|
|
215
|
+
NSLocalizedDescriptionKey : @"Failed to sign challenge"
|
|
216
|
+
}];
|
|
217
|
+
RJLogError(@"Failed to sign challenge");
|
|
218
|
+
if (completion) {
|
|
219
|
+
completion(NO, nil, 0, signError);
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
[self startSessionWithChallenge:challenge
|
|
226
|
+
nonce:nonce
|
|
227
|
+
signature:signature
|
|
228
|
+
completion:completion];
|
|
229
|
+
}];
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
- (nullable NSString *)deviceCredentialId {
|
|
235
|
+
return self.cachedDeviceCredentialId;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
- (nullable NSString *)currentUploadToken {
|
|
239
|
+
if ([self hasValidUploadToken]) {
|
|
240
|
+
return self.cachedUploadToken;
|
|
241
|
+
}
|
|
242
|
+
return nil;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
- (BOOL)hasValidUploadToken {
|
|
246
|
+
if (!self.cachedUploadToken || !self.uploadTokenExpiry) {
|
|
247
|
+
return NO;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
NSTimeInterval timeUntilExpiry =
|
|
252
|
+
[self.uploadTokenExpiry timeIntervalSinceNow];
|
|
253
|
+
return timeUntilExpiry > 60.0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
- (void)clearAllAuthData {
|
|
257
|
+
[self deletePrivateKey];
|
|
258
|
+
|
|
259
|
+
RJKeychainManager *keychain = [RJKeychainManager sharedManager];
|
|
260
|
+
[keychain deleteValueForKey:kDeviceCredentialKey];
|
|
261
|
+
[keychain deleteValueForKey:kUploadTokenKey];
|
|
262
|
+
[keychain deleteValueForKey:kUploadTokenExpiryKey];
|
|
263
|
+
|
|
264
|
+
self.cachedDeviceCredentialId = nil;
|
|
265
|
+
self.cachedUploadToken = nil;
|
|
266
|
+
self.uploadTokenExpiry = nil;
|
|
267
|
+
|
|
268
|
+
RJLogDebug(@"Cleared all device auth data");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#pragma mark - ECDSA Keypair Management
|
|
272
|
+
|
|
273
|
+
- (nullable SecKeyRef)getOrCreatePrivateKey {
|
|
274
|
+
return [self getOrCreatePrivateKeyWithRetry:YES];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
- (nullable SecKeyRef)getOrCreatePrivateKeyWithRetry:(BOOL)shouldRetryOnFailure {
|
|
278
|
+
|
|
279
|
+
SecKeyRef privateKey = [self loadPrivateKeyFromKeychain];
|
|
280
|
+
if (privateKey) {
|
|
281
|
+
|
|
282
|
+
SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey);
|
|
283
|
+
if (publicKey) {
|
|
284
|
+
CFRelease(publicKey);
|
|
285
|
+
RJLogDebug(@"Loaded existing ECDSA private key");
|
|
286
|
+
return privateKey;
|
|
287
|
+
} else {
|
|
288
|
+
|
|
289
|
+
RJLogWarning(@"Existing private key is corrupted, deleting and regenerating");
|
|
290
|
+
CFRelease(privateKey);
|
|
291
|
+
[self deletePrivateKey];
|
|
292
|
+
[self clearAllAuthData];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
RJLogDebug(@"Generating new ECDSA P-256 keypair");
|
|
298
|
+
|
|
299
|
+
NSDictionary *attributes = @{
|
|
300
|
+
(__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeECSECPrimeRandom,
|
|
301
|
+
(__bridge id)kSecAttrKeySizeInBits : @256,
|
|
302
|
+
(__bridge id)kSecPrivateKeyAttrs : @{
|
|
303
|
+
(__bridge id)kSecAttrIsPermanent : @YES,
|
|
304
|
+
(__bridge id)kSecAttrApplicationTag :
|
|
305
|
+
[kKeychainKeyTag dataUsingEncoding:NSUTF8StringEncoding],
|
|
306
|
+
(__bridge id)kSecAttrAccessible :
|
|
307
|
+
(__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
CFErrorRef error = NULL;
|
|
312
|
+
privateKey =
|
|
313
|
+
SecKeyCreateRandomKey((__bridge CFDictionaryRef)attributes, &error);
|
|
314
|
+
|
|
315
|
+
if (error) {
|
|
316
|
+
CFStringRef errorDesc = CFErrorCopyDescription(error);
|
|
317
|
+
RJLogError(@"Failed to generate ECDSA key: %@",
|
|
318
|
+
(__bridge NSString *)errorDesc);
|
|
319
|
+
if (errorDesc)
|
|
320
|
+
CFRelease(errorDesc);
|
|
321
|
+
CFRelease(error);
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
if (shouldRetryOnFailure) {
|
|
326
|
+
RJLogDebug(@"Attempting recovery: deleting existing key and retrying generation");
|
|
327
|
+
[self deletePrivateKey];
|
|
328
|
+
return [self getOrCreatePrivateKeyWithRetry:NO];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return NULL;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!privateKey) {
|
|
335
|
+
RJLogError(@"SecKeyCreateRandomKey returned NULL");
|
|
336
|
+
return NULL;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
RJLogDebug(@"Successfully generated ECDSA P-256 keypair");
|
|
340
|
+
return privateKey;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
- (nullable SecKeyRef)loadPrivateKeyFromKeychain {
|
|
344
|
+
NSDictionary *query = @{
|
|
345
|
+
(__bridge id)kSecClass : (__bridge id)kSecClassKey,
|
|
346
|
+
(__bridge id)kSecAttrApplicationTag :
|
|
347
|
+
[kKeychainKeyTag dataUsingEncoding:NSUTF8StringEncoding],
|
|
348
|
+
(__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeECSECPrimeRandom,
|
|
349
|
+
(__bridge id)kSecReturnRef : @YES
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
SecKeyRef privateKey = NULL;
|
|
353
|
+
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query,
|
|
354
|
+
(CFTypeRef *)&privateKey);
|
|
355
|
+
|
|
356
|
+
if (status == errSecSuccess && privateKey) {
|
|
357
|
+
return privateKey;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return NULL;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
- (void)deletePrivateKey {
|
|
364
|
+
NSDictionary *query = @{
|
|
365
|
+
(__bridge id)kSecClass : (__bridge id)kSecClassKey,
|
|
366
|
+
(__bridge id)kSecAttrApplicationTag :
|
|
367
|
+
[kKeychainKeyTag dataUsingEncoding:NSUTF8StringEncoding],
|
|
368
|
+
(__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeECSECPrimeRandom
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
|
|
372
|
+
if (status == errSecSuccess) {
|
|
373
|
+
RJLogDebug(@"Deleted private key from Keychain");
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
- (nullable NSString *)exportPublicKeyToPEM:(SecKeyRef)publicKey {
|
|
378
|
+
CFErrorRef error = NULL;
|
|
379
|
+
CFDataRef publicKeyDataRef =
|
|
380
|
+
SecKeyCopyExternalRepresentation(publicKey, &error);
|
|
381
|
+
|
|
382
|
+
if (error) {
|
|
383
|
+
CFStringRef errorDesc = CFErrorCopyDescription(error);
|
|
384
|
+
RJLogError(@"Failed to export public key: %@",
|
|
385
|
+
(__bridge NSString *)errorDesc);
|
|
386
|
+
if (errorDesc)
|
|
387
|
+
CFRelease(errorDesc);
|
|
388
|
+
CFRelease(error);
|
|
389
|
+
return nil;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (!publicKeyDataRef) {
|
|
393
|
+
RJLogError(@"Public key data is nil");
|
|
394
|
+
return nil;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
NSData *publicKeyData = (__bridge NSData *)publicKeyDataRef;
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
NSString *base64 = [publicKeyData
|
|
402
|
+
base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
NSString *pem = [NSString
|
|
406
|
+
stringWithFormat:
|
|
407
|
+
@"-----BEGIN PUBLIC KEY-----\n%@\n-----END PUBLIC KEY-----", base64];
|
|
408
|
+
|
|
409
|
+
CFRelease(publicKeyDataRef);
|
|
410
|
+
|
|
411
|
+
return pem;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
- (nullable NSString *)signChallenge:(NSString *)challenge {
|
|
415
|
+
|
|
416
|
+
SecKeyRef privateKey = [self loadPrivateKeyFromKeychain];
|
|
417
|
+
if (!privateKey) {
|
|
418
|
+
RJLogError(@"Private key not found");
|
|
419
|
+
return nil;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
NSData *challengeData = [[NSData alloc] initWithBase64EncodedString:challenge
|
|
424
|
+
options:0];
|
|
425
|
+
if (!challengeData) {
|
|
426
|
+
CFRelease(privateKey);
|
|
427
|
+
RJLogError(@"Invalid challenge base64");
|
|
428
|
+
return nil;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
CFErrorRef error = NULL;
|
|
433
|
+
CFDataRef signatureRef = SecKeyCreateSignature(
|
|
434
|
+
privateKey, kSecKeyAlgorithmECDSASignatureMessageX962SHA256,
|
|
435
|
+
(__bridge CFDataRef)challengeData, &error);
|
|
436
|
+
|
|
437
|
+
CFRelease(privateKey);
|
|
438
|
+
|
|
439
|
+
if (error) {
|
|
440
|
+
CFStringRef errorDesc = CFErrorCopyDescription(error);
|
|
441
|
+
RJLogError(@"Failed to sign challenge: %@", (__bridge NSString *)errorDesc);
|
|
442
|
+
if (errorDesc)
|
|
443
|
+
CFRelease(errorDesc);
|
|
444
|
+
CFRelease(error);
|
|
445
|
+
return nil;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (!signatureRef) {
|
|
449
|
+
RJLogError(@"Signature is nil");
|
|
450
|
+
return nil;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
NSData *signature = (__bridge NSData *)signatureRef;
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
NSString *base64Sig = [signature base64EncodedStringWithOptions:0];
|
|
457
|
+
|
|
458
|
+
CFRelease(signatureRef);
|
|
459
|
+
|
|
460
|
+
return base64Sig;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
#pragma mark - Backend Communication
|
|
464
|
+
|
|
465
|
+
- (void)registerWithBackend:(NSString *)projectPublicKey
|
|
466
|
+
bundleId:(NSString *)bundleId
|
|
467
|
+
platform:(NSString *)platform
|
|
468
|
+
sdkVersion:(NSString *)sdkVersion
|
|
469
|
+
publicKeyPEM:(NSString *)publicKeyPEM
|
|
470
|
+
completion:(RJDeviceAuthCompletionHandler)completion {
|
|
471
|
+
|
|
472
|
+
NSString *urlString =
|
|
473
|
+
[NSString stringWithFormat:@"%@/api/devices/register", self.apiUrl];
|
|
474
|
+
RJLogDebug(@"Register URL: %@", urlString);
|
|
475
|
+
NSURL *url = [NSURL URLWithString:urlString];
|
|
476
|
+
|
|
477
|
+
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
|
|
478
|
+
request.HTTPMethod = @"POST";
|
|
479
|
+
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
|
|
480
|
+
|
|
481
|
+
NSDictionary *body = @{
|
|
482
|
+
@"projectPublicKey" : projectPublicKey,
|
|
483
|
+
@"bundleId" : bundleId,
|
|
484
|
+
@"platform" : platform,
|
|
485
|
+
@"sdkVersion" : sdkVersion,
|
|
486
|
+
@"devicePublicKey" : publicKeyPEM
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
NSError *jsonError;
|
|
490
|
+
request.HTTPBody = [NSJSONSerialization dataWithJSONObject:body
|
|
491
|
+
options:0
|
|
492
|
+
error:&jsonError];
|
|
493
|
+
|
|
494
|
+
if (jsonError) {
|
|
495
|
+
RJLogError(@"Failed to serialize registration request: %@", jsonError);
|
|
496
|
+
if (completion) {
|
|
497
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
498
|
+
completion(NO, nil, jsonError);
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
RJLogDebug(@"Registering device with backend...");
|
|
505
|
+
|
|
506
|
+
NSURLSessionDataTask *task = [[NSURLSession sharedSession]
|
|
507
|
+
dataTaskWithRequest:request
|
|
508
|
+
completionHandler:^(NSData *data, NSURLResponse *response,
|
|
509
|
+
NSError *error) {
|
|
510
|
+
if (error) {
|
|
511
|
+
RJLogError(@"Registration request failed: %@", error);
|
|
512
|
+
if (completion) {
|
|
513
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
514
|
+
completion(NO, nil, error);
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
|
|
521
|
+
if (httpResponse.statusCode != 200) {
|
|
522
|
+
NSString *errorMsg = [NSString
|
|
523
|
+
stringWithFormat:@"Registration failed with status %ld",
|
|
524
|
+
(long)httpResponse.statusCode];
|
|
525
|
+
NSError *httpError = [NSError
|
|
526
|
+
errorWithDomain:@"RJDeviceAuth"
|
|
527
|
+
code:httpResponse.statusCode
|
|
528
|
+
userInfo:@{NSLocalizedDescriptionKey : errorMsg}];
|
|
529
|
+
RJLogError(@"%@", errorMsg);
|
|
530
|
+
if (completion) {
|
|
531
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
532
|
+
completion(NO, nil, httpError);
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
NSError *parseError;
|
|
539
|
+
NSDictionary *json =
|
|
540
|
+
[NSJSONSerialization JSONObjectWithData:data
|
|
541
|
+
options:0
|
|
542
|
+
error:&parseError];
|
|
543
|
+
|
|
544
|
+
if (parseError || !json[@"deviceCredentialId"]) {
|
|
545
|
+
RJLogError(@"Failed to parse registration response: %@",
|
|
546
|
+
parseError);
|
|
547
|
+
if (completion) {
|
|
548
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
549
|
+
completion(NO, nil, parseError);
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
NSString *credentialId = json[@"deviceCredentialId"];
|
|
556
|
+
[self saveDeviceCredentialId:credentialId];
|
|
557
|
+
|
|
558
|
+
RJLogDebug(@"Device registered: %@", credentialId);
|
|
559
|
+
|
|
560
|
+
if (completion) {
|
|
561
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
562
|
+
completion(YES, credentialId, nil);
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}];
|
|
566
|
+
|
|
567
|
+
[task resume];
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
- (void)requestChallengeWithCompletion:
|
|
571
|
+
(void (^)(BOOL success, NSString *_Nullable challenge,
|
|
572
|
+
NSString *_Nullable nonce, NSError *_Nullable error))completion {
|
|
573
|
+
|
|
574
|
+
NSString *urlString = [NSString
|
|
575
|
+
stringWithFormat:@"%@/api/devices/challenge", self.apiUrl];
|
|
576
|
+
RJLogDebug(@"Challenge URL: %@", urlString);
|
|
577
|
+
NSURL *url = [NSURL URLWithString:urlString];
|
|
578
|
+
|
|
579
|
+
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
|
|
580
|
+
request.HTTPMethod = @"POST";
|
|
581
|
+
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
|
|
582
|
+
|
|
583
|
+
NSDictionary *body = @{@"deviceCredentialId" : self.cachedDeviceCredentialId};
|
|
584
|
+
|
|
585
|
+
NSError *jsonError;
|
|
586
|
+
request.HTTPBody = [NSJSONSerialization dataWithJSONObject:body
|
|
587
|
+
options:0
|
|
588
|
+
error:&jsonError];
|
|
589
|
+
|
|
590
|
+
if (jsonError) {
|
|
591
|
+
RJLogError(@"Failed to serialize challenge request: %@", jsonError);
|
|
592
|
+
if (completion) {
|
|
593
|
+
completion(NO, nil, nil, jsonError);
|
|
594
|
+
}
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
RJLogDebug(@"Requesting challenge from backend...");
|
|
599
|
+
|
|
600
|
+
NSURLSessionDataTask *task = [[NSURLSession sharedSession]
|
|
601
|
+
dataTaskWithRequest:request
|
|
602
|
+
completionHandler:^(NSData *data, NSURLResponse *response,
|
|
603
|
+
NSError *error) {
|
|
604
|
+
if (error) {
|
|
605
|
+
RJLogError(@"Challenge request failed: %@", error);
|
|
606
|
+
if (completion) {
|
|
607
|
+
completion(NO, nil, nil, error);
|
|
608
|
+
}
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
|
|
613
|
+
if (httpResponse.statusCode != 200) {
|
|
614
|
+
NSString *errorMsg = [NSString
|
|
615
|
+
stringWithFormat:@"Challenge request failed with status %ld",
|
|
616
|
+
(long)httpResponse.statusCode];
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
if (data) {
|
|
620
|
+
NSString *body =
|
|
621
|
+
[[NSString alloc] initWithData:data
|
|
622
|
+
encoding:NSUTF8StringEncoding];
|
|
623
|
+
RJLogError(@"Error response body: %@", body);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
if (httpResponse.statusCode == 404 || httpResponse.statusCode == 403) {
|
|
629
|
+
RJLogDebug(@"Device credential rejected by backend (%ld) - "
|
|
630
|
+
@"clearing local credentials",
|
|
631
|
+
(long)httpResponse.statusCode);
|
|
632
|
+
[self clearAllAuthData];
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
NSError *httpError = [NSError
|
|
636
|
+
errorWithDomain:@"RJDeviceAuth"
|
|
637
|
+
code:httpResponse.statusCode
|
|
638
|
+
userInfo:@{NSLocalizedDescriptionKey : errorMsg}];
|
|
639
|
+
RJLogError(@"%@", errorMsg);
|
|
640
|
+
if (completion) {
|
|
641
|
+
completion(NO, nil, nil, httpError);
|
|
642
|
+
}
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
NSError *parseError;
|
|
647
|
+
NSDictionary *json =
|
|
648
|
+
[NSJSONSerialization JSONObjectWithData:data
|
|
649
|
+
options:0
|
|
650
|
+
error:&parseError];
|
|
651
|
+
|
|
652
|
+
if (parseError || !json[@"challenge"] || !json[@"nonce"]) {
|
|
653
|
+
RJLogError(@"Failed to parse challenge response: %@", parseError);
|
|
654
|
+
if (completion) {
|
|
655
|
+
completion(NO, nil, nil, parseError);
|
|
656
|
+
}
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
NSString *challenge = json[@"challenge"];
|
|
661
|
+
NSString *nonce = json[@"nonce"];
|
|
662
|
+
|
|
663
|
+
RJLogDebug(@"Received challenge from backend");
|
|
664
|
+
|
|
665
|
+
if (completion) {
|
|
666
|
+
completion(YES, challenge, nonce, nil);
|
|
667
|
+
}
|
|
668
|
+
}];
|
|
669
|
+
|
|
670
|
+
[task resume];
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
- (void)startSessionWithChallenge:(NSString *)challenge
|
|
674
|
+
nonce:(NSString *)nonce
|
|
675
|
+
signature:(NSString *)signature
|
|
676
|
+
completion:(RJDeviceTokenCompletionHandler)completion {
|
|
677
|
+
|
|
678
|
+
NSString *urlString = [NSString
|
|
679
|
+
stringWithFormat:@"%@/api/devices/start-session", self.apiUrl];
|
|
680
|
+
RJLogDebug(@"Start Session URL: %@", urlString);
|
|
681
|
+
NSURL *url = [NSURL URLWithString:urlString];
|
|
682
|
+
|
|
683
|
+
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
|
|
684
|
+
request.HTTPMethod = @"POST";
|
|
685
|
+
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
|
|
686
|
+
|
|
687
|
+
NSDictionary *body = @{
|
|
688
|
+
@"deviceCredentialId" : self.cachedDeviceCredentialId,
|
|
689
|
+
@"challenge" : challenge,
|
|
690
|
+
@"signature" : signature,
|
|
691
|
+
@"nonce" : nonce
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
NSError *jsonError;
|
|
695
|
+
request.HTTPBody = [NSJSONSerialization dataWithJSONObject:body
|
|
696
|
+
options:0
|
|
697
|
+
error:&jsonError];
|
|
698
|
+
|
|
699
|
+
if (jsonError) {
|
|
700
|
+
RJLogError(@"Failed to serialize start-session request: %@", jsonError);
|
|
701
|
+
if (completion) {
|
|
702
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
703
|
+
completion(NO, nil, 0, jsonError);
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
RJLogDebug(@"Starting session with signed challenge...");
|
|
710
|
+
|
|
711
|
+
NSURLSessionDataTask *task = [[NSURLSession sharedSession]
|
|
712
|
+
dataTaskWithRequest:request
|
|
713
|
+
completionHandler:^(NSData *data, NSURLResponse *response,
|
|
714
|
+
NSError *error) {
|
|
715
|
+
if (error) {
|
|
716
|
+
RJLogError(@"Start-session request failed: %@", error);
|
|
717
|
+
if (completion) {
|
|
718
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
719
|
+
completion(NO, nil, 0, error);
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
|
|
726
|
+
if (httpResponse.statusCode != 200) {
|
|
727
|
+
NSString *errorMsg = [NSString
|
|
728
|
+
stringWithFormat:@"Start-session failed with status %ld",
|
|
729
|
+
(long)httpResponse.statusCode];
|
|
730
|
+
NSError *httpError = [NSError
|
|
731
|
+
errorWithDomain:@"RJDeviceAuth"
|
|
732
|
+
code:httpResponse.statusCode
|
|
733
|
+
userInfo:@{NSLocalizedDescriptionKey : errorMsg}];
|
|
734
|
+
RJLogError(@"%@", errorMsg);
|
|
735
|
+
if (completion) {
|
|
736
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
737
|
+
completion(NO, nil, 0, httpError);
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
NSError *parseError;
|
|
744
|
+
NSDictionary *json =
|
|
745
|
+
[NSJSONSerialization JSONObjectWithData:data
|
|
746
|
+
options:0
|
|
747
|
+
error:&parseError];
|
|
748
|
+
|
|
749
|
+
if (parseError || !json[@"uploadToken"]) {
|
|
750
|
+
RJLogError(@"Failed to parse start-session response: %@",
|
|
751
|
+
parseError);
|
|
752
|
+
if (completion) {
|
|
753
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
754
|
+
completion(NO, nil, 0, parseError);
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
NSString *uploadToken = json[@"uploadToken"];
|
|
761
|
+
NSInteger expiresIn = [json[@"expiresIn"] integerValue];
|
|
762
|
+
|
|
763
|
+
[self saveUploadToken:uploadToken expiresIn:expiresIn];
|
|
764
|
+
|
|
765
|
+
RJLogDebug(@"Got upload token (expires in %ld seconds)",
|
|
766
|
+
(long)expiresIn);
|
|
767
|
+
|
|
768
|
+
if (completion) {
|
|
769
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
770
|
+
completion(YES, uploadToken, expiresIn, nil);
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
}];
|
|
774
|
+
|
|
775
|
+
[task resume];
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
#pragma mark - Storage
|
|
779
|
+
|
|
780
|
+
- (void)loadStoredCredentials {
|
|
781
|
+
RJKeychainManager *keychain = [RJKeychainManager sharedManager];
|
|
782
|
+
|
|
783
|
+
self.cachedDeviceCredentialId = [keychain stringForKey:kDeviceCredentialKey];
|
|
784
|
+
self.cachedUploadToken = [keychain stringForKey:kUploadTokenKey];
|
|
785
|
+
|
|
786
|
+
NSString *expiryString = [keychain stringForKey:kUploadTokenExpiryKey];
|
|
787
|
+
if (expiryString) {
|
|
788
|
+
self.uploadTokenExpiry =
|
|
789
|
+
[NSDate dateWithTimeIntervalSince1970:[expiryString doubleValue]];
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (self.cachedDeviceCredentialId) {
|
|
793
|
+
RJLogDebug(@"Loaded stored device credential: %@",
|
|
794
|
+
self.cachedDeviceCredentialId);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
- (void)saveDeviceCredentialId:(NSString *)credentialId {
|
|
799
|
+
self.cachedDeviceCredentialId = credentialId;
|
|
800
|
+
[[RJKeychainManager sharedManager] setString:credentialId
|
|
801
|
+
forKey:kDeviceCredentialKey];
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
- (void)saveUploadToken:(NSString *)token expiresIn:(NSInteger)expiresIn {
|
|
805
|
+
self.cachedUploadToken = token;
|
|
806
|
+
self.uploadTokenExpiry = [NSDate dateWithTimeIntervalSinceNow:expiresIn];
|
|
807
|
+
|
|
808
|
+
RJKeychainManager *keychain = [RJKeychainManager sharedManager];
|
|
809
|
+
[keychain setString:token forKey:kUploadTokenKey];
|
|
810
|
+
[keychain setString:[@([self.uploadTokenExpiry timeIntervalSince1970]) stringValue]
|
|
811
|
+
forKey:kUploadTokenExpiryKey];
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
#pragma mark - Auto Registration
|
|
815
|
+
|
|
816
|
+
- (BOOL)canAutoRegister {
|
|
817
|
+
return self.projectPublicKey.length > 0 &&
|
|
818
|
+
self.storedBundleId.length > 0 &&
|
|
819
|
+
self.apiUrl.length > 0;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
- (BOOL)isDeviceRegistered {
|
|
823
|
+
return self.cachedDeviceCredentialId.length > 0;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
- (void)getUploadTokenWithAutoRegisterCompletion:(RJDeviceTokenCompletionHandler)completion {
|
|
827
|
+
|
|
828
|
+
if ([self hasValidUploadToken]) {
|
|
829
|
+
NSTimeInterval remainingTime = [self.uploadTokenExpiry timeIntervalSinceNow];
|
|
830
|
+
RJLogDebug(@"Using cached upload token (expires in %ld seconds)", (long)remainingTime);
|
|
831
|
+
if (completion) {
|
|
832
|
+
completion(YES, self.cachedUploadToken, (NSInteger)remainingTime, nil);
|
|
833
|
+
}
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
if (self.cachedDeviceCredentialId) {
|
|
839
|
+
[self getUploadTokenWithCompletion:completion];
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
if (![self canAutoRegister]) {
|
|
845
|
+
NSError *error = [NSError
|
|
846
|
+
errorWithDomain:@"RJDeviceAuth"
|
|
847
|
+
code:2003
|
|
848
|
+
userInfo:@{
|
|
849
|
+
NSLocalizedDescriptionKey : @"Device not registered and auto-registration not configured"
|
|
850
|
+
}];
|
|
851
|
+
RJLogError(@"Cannot auto-register: missing registration parameters");
|
|
852
|
+
if (completion) {
|
|
853
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
854
|
+
completion(NO, nil, 0, error);
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Check cooldown after consecutive failures to prevent flooding
|
|
861
|
+
NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
|
|
862
|
+
if (self.consecutiveFailures > 0 && self.lastFailedRegistrationTime > 0) {
|
|
863
|
+
// Exponential backoff: 5s, 10s, 20s, 40s... up to 5 minutes
|
|
864
|
+
NSTimeInterval cooldown = MIN(
|
|
865
|
+
RJ_AUTH_COOLDOWN_BASE_SECONDS * pow(2, self.consecutiveFailures - 1),
|
|
866
|
+
RJ_AUTH_COOLDOWN_MAX_SECONDS
|
|
867
|
+
);
|
|
868
|
+
NSTimeInterval timeSinceLastFailure = now - self.lastFailedRegistrationTime;
|
|
869
|
+
|
|
870
|
+
if (timeSinceLastFailure < cooldown) {
|
|
871
|
+
NSTimeInterval remainingCooldown = cooldown - timeSinceLastFailure;
|
|
872
|
+
RJLogDebug(@"Auto-registration in cooldown (%.1fs remaining after %ld failures)",
|
|
873
|
+
remainingCooldown, (long)self.consecutiveFailures);
|
|
874
|
+
|
|
875
|
+
NSError *cooldownError = [NSError
|
|
876
|
+
errorWithDomain:@"RJDeviceAuth"
|
|
877
|
+
code:429
|
|
878
|
+
userInfo:@{NSLocalizedDescriptionKey :
|
|
879
|
+
[NSString stringWithFormat:@"Rate limited - retry in %.0fs", remainingCooldown]}];
|
|
880
|
+
if (completion) {
|
|
881
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
882
|
+
completion(NO, nil, 0, cooldownError);
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
@synchronized(self.pendingTokenCallbacks) {
|
|
891
|
+
if (completion) {
|
|
892
|
+
[self.pendingTokenCallbacks addObject:[completion copy]];
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
if (self.registrationInProgress) {
|
|
897
|
+
RJLogDebug(@"Auto-registration already in progress, callback queued");
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
self.registrationInProgress = YES;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
RJLogDebug(@"Device not registered - starting automatic re-registration...");
|
|
905
|
+
|
|
906
|
+
__weak __typeof__(self) weakSelf = self;
|
|
907
|
+
[self registerDeviceWithProjectKey:self.projectPublicKey
|
|
908
|
+
bundleId:self.storedBundleId
|
|
909
|
+
platform:self.storedPlatform ?: @"ios"
|
|
910
|
+
sdkVersion:self.storedSdkVersion ?: @"1.0.0"
|
|
911
|
+
apiUrl:self.apiUrl
|
|
912
|
+
completion:^(BOOL success, NSString *credId, NSError *error) {
|
|
913
|
+
__strong __typeof__(weakSelf) strongSelf = weakSelf;
|
|
914
|
+
if (!strongSelf) return;
|
|
915
|
+
|
|
916
|
+
if (!success) {
|
|
917
|
+
RJLogError(@"Auto-registration failed: %@", error);
|
|
918
|
+
// Track consecutive failures for exponential backoff
|
|
919
|
+
strongSelf.consecutiveFailures++;
|
|
920
|
+
strongSelf.lastFailedRegistrationTime = [[NSDate date] timeIntervalSince1970];
|
|
921
|
+
|
|
922
|
+
if (strongSelf.consecutiveFailures >= RJ_AUTH_MAX_CONSECUTIVE_FAILURES) {
|
|
923
|
+
RJLogError(@"Auto-registration failed %ld times - backing off significantly",
|
|
924
|
+
(long)strongSelf.consecutiveFailures);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
[strongSelf notifyPendingCallbacksWithSuccess:NO token:nil expiresIn:0 error:error];
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Success! Reset failure tracking
|
|
932
|
+
strongSelf.consecutiveFailures = 0;
|
|
933
|
+
strongSelf.lastFailedRegistrationTime = 0;
|
|
934
|
+
|
|
935
|
+
RJLogDebug(@"Auto-registration successful (credential: %@), fetching upload token...", credId);
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
[strongSelf getUploadTokenWithCompletion:^(BOOL tokenSuccess, NSString *token,
|
|
939
|
+
NSInteger expiresIn, NSError *tokenError) {
|
|
940
|
+
[strongSelf notifyPendingCallbacksWithSuccess:tokenSuccess
|
|
941
|
+
token:token
|
|
942
|
+
expiresIn:expiresIn
|
|
943
|
+
error:tokenError];
|
|
944
|
+
}];
|
|
945
|
+
}];
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
- (void)notifyPendingCallbacksWithSuccess:(BOOL)success
|
|
949
|
+
token:(NSString *)token
|
|
950
|
+
expiresIn:(NSInteger)expiresIn
|
|
951
|
+
error:(NSError *)error {
|
|
952
|
+
NSArray<RJDeviceTokenCompletionHandler> *callbacks;
|
|
953
|
+
|
|
954
|
+
@synchronized(self.pendingTokenCallbacks) {
|
|
955
|
+
self.registrationInProgress = NO;
|
|
956
|
+
callbacks = [self.pendingTokenCallbacks copy];
|
|
957
|
+
[self.pendingTokenCallbacks removeAllObjects];
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
961
|
+
for (RJDeviceTokenCompletionHandler callback in callbacks) {
|
|
962
|
+
callback(success, token, expiresIn, error);
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
@end
|