@maccesar/titools 2.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/AGENTS-TEMPLATE.md +173 -0
- package/README.md +867 -0
- package/agents/ti-researcher.md +108 -0
- package/bin/titools.js +53 -0
- package/lib/commands/agents.js +126 -0
- package/lib/commands/install.js +188 -0
- package/lib/commands/uninstall.js +215 -0
- package/lib/commands/update.js +159 -0
- package/lib/config.js +119 -0
- package/lib/downloader.js +153 -0
- package/lib/installer.js +253 -0
- package/lib/platform.js +108 -0
- package/lib/symlink.js +142 -0
- package/lib/utils.js +270 -0
- package/package.json +67 -0
- package/skills/alloy-expert/SKILL.md +247 -0
- package/skills/alloy-expert/assets/ControllerAutoCleanup.js +182 -0
- package/skills/alloy-expert/references/alloy-structure.md +381 -0
- package/skills/alloy-expert/references/anti-patterns.md +133 -0
- package/skills/alloy-expert/references/code-conventions.md +469 -0
- package/skills/alloy-expert/references/contracts.md +280 -0
- package/skills/alloy-expert/references/controller-patterns.md +520 -0
- package/skills/alloy-expert/references/error-handling.md +484 -0
- package/skills/alloy-expert/references/examples.md +735 -0
- package/skills/alloy-expert/references/migration-patterns.md +298 -0
- package/skills/alloy-expert/references/patterns.md +448 -0
- package/skills/alloy-expert/references/performance-patterns.md +855 -0
- package/skills/alloy-expert/references/security-patterns.md +847 -0
- package/skills/alloy-expert/references/state-management.md +779 -0
- package/skills/alloy-expert/references/testing.md +872 -0
- package/skills/alloy-guides/SKILL.md +214 -0
- package/skills/alloy-guides/references/CLI_TASKS.md +243 -0
- package/skills/alloy-guides/references/CONCEPTS.md +191 -0
- package/skills/alloy-guides/references/CONTROLLERS.md +298 -0
- package/skills/alloy-guides/references/MODELS.md +1028 -0
- package/skills/alloy-guides/references/PURGETSS.md +56 -0
- package/skills/alloy-guides/references/VIEWS_DYNAMIC.md +242 -0
- package/skills/alloy-guides/references/VIEWS_STYLES.md +388 -0
- package/skills/alloy-guides/references/VIEWS_WITHOUT_CONTROLLERS.md +109 -0
- package/skills/alloy-guides/references/VIEWS_XML.md +558 -0
- package/skills/alloy-guides/references/WIDGETS.md +176 -0
- package/skills/alloy-howtos/SKILL.md +203 -0
- package/skills/alloy-howtos/references/best_practices.md +138 -0
- package/skills/alloy-howtos/references/cli_reference.md +253 -0
- package/skills/alloy-howtos/references/config_files.md +87 -0
- package/skills/alloy-howtos/references/custom_tags.md +147 -0
- package/skills/alloy-howtos/references/debugging_troubleshooting.md +101 -0
- package/skills/alloy-howtos/references/samples.md +167 -0
- package/skills/purgetss/SKILL.md +442 -0
- package/skills/purgetss/assets/purgetss.config.cjs +17 -0
- package/skills/purgetss/references/EXAMPLES.md +247 -0
- package/skills/purgetss/references/animation-system.md +1294 -0
- package/skills/purgetss/references/apply-directive.md +375 -0
- package/skills/purgetss/references/arbitrary-values.md +612 -0
- package/skills/purgetss/references/class-index.md +1350 -0
- package/skills/purgetss/references/cli-commands.md +948 -0
- package/skills/purgetss/references/configurable-properties.md +654 -0
- package/skills/purgetss/references/custom-rules.md +161 -0
- package/skills/purgetss/references/customization-deep-dive.md +722 -0
- package/skills/purgetss/references/dynamic-component-creation.md +489 -0
- package/skills/purgetss/references/grid-layout.md +455 -0
- package/skills/purgetss/references/icon-fonts.md +609 -0
- package/skills/purgetss/references/installation-setup.md +366 -0
- package/skills/purgetss/references/opacity-modifier.md +291 -0
- package/skills/purgetss/references/platform-modifiers.md +479 -0
- package/skills/purgetss/references/smart-mappings.md +42 -0
- package/skills/purgetss/references/titanium-resets.md +359 -0
- package/skills/purgetss/references/ui-ux-design.md +1526 -0
- package/skills/ti-guides/SKILL.md +94 -0
- package/skills/ti-guides/references/advanced-data-and-images.md +19 -0
- package/skills/ti-guides/references/alloy-cli-advanced.md +84 -0
- package/skills/ti-guides/references/alloy-data-mastery.md +29 -0
- package/skills/ti-guides/references/alloy-widgets-and-themes.md +19 -0
- package/skills/ti-guides/references/android-manifest.md +97 -0
- package/skills/ti-guides/references/app-distribution.md +258 -0
- package/skills/ti-guides/references/application-frameworks.md +377 -0
- package/skills/ti-guides/references/cli-reference.md +402 -0
- package/skills/ti-guides/references/coding-best-practices.md +102 -0
- package/skills/ti-guides/references/commonjs-advanced.md +134 -0
- package/skills/ti-guides/references/hello-world.md +100 -0
- package/skills/ti-guides/references/hyperloop-native-access.md +62 -0
- package/skills/ti-guides/references/javascript-primer.md +411 -0
- package/skills/ti-guides/references/reserved-words.md +36 -0
- package/skills/ti-guides/references/resources.md +183 -0
- package/skills/ti-guides/references/style-and-conventions.md +48 -0
- package/skills/ti-guides/references/tiapp-config.md +609 -0
- package/skills/ti-howtos/SKILL.md +174 -0
- package/skills/ti-howtos/references/android-platform-deep-dives.md +658 -0
- package/skills/ti-howtos/references/automation-fastlane-appium.md +95 -0
- package/skills/ti-howtos/references/buffer-codec-streams.md +140 -0
- package/skills/ti-howtos/references/cross-platform-development.md +348 -0
- package/skills/ti-howtos/references/debugging-profiling.md +543 -0
- package/skills/ti-howtos/references/extending-titanium.md +723 -0
- package/skills/ti-howtos/references/google-maps-v2.md +169 -0
- package/skills/ti-howtos/references/ios-map-kit.md +143 -0
- package/skills/ti-howtos/references/ios-platform-deep-dives.md +783 -0
- package/skills/ti-howtos/references/local-data-sources.md +301 -0
- package/skills/ti-howtos/references/location-and-maps.md +252 -0
- package/skills/ti-howtos/references/media-apis.md +210 -0
- package/skills/ti-howtos/references/notification-services.md +599 -0
- package/skills/ti-howtos/references/remote-data-sources.md +349 -0
- package/skills/ti-howtos/references/tutorials.md +502 -0
- package/skills/ti-howtos/references/using-modules.md +237 -0
- package/skills/ti-howtos/references/web-content-integration.md +307 -0
- package/skills/ti-howtos/references/webpack-build-pipeline.md +78 -0
- package/skills/ti-ui/SKILL.md +179 -0
- package/skills/ti-ui/references/accessibility-deep-dive.md +242 -0
- package/skills/ti-ui/references/animation-and-matrices.md +599 -0
- package/skills/ti-ui/references/application-structures.md +655 -0
- package/skills/ti-ui/references/custom-fonts-styling.md +579 -0
- package/skills/ti-ui/references/event-handling.md +393 -0
- package/skills/ti-ui/references/gestures.md +473 -0
- package/skills/ti-ui/references/icons-and-splash-screens.md +409 -0
- package/skills/ti-ui/references/layouts-and-positioning.md +462 -0
- package/skills/ti-ui/references/listviews-and-performance.md +619 -0
- package/skills/ti-ui/references/orientation.md +362 -0
- package/skills/ti-ui/references/platform-ui-android.md +635 -0
- package/skills/ti-ui/references/platform-ui-ios.md +469 -0
- package/skills/ti-ui/references/scrolling-views.md +252 -0
- package/skills/ti-ui/references/tableviews.md +568 -0
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
# Security Patterns for Titanium Mobile Apps
|
|
2
|
+
|
|
3
|
+
## Token Storage Strategy
|
|
4
|
+
|
|
5
|
+
**NEVER store tokens in:** `Ti.App.Properties` (plaintext), localStorage, or files.
|
|
6
|
+
|
|
7
|
+
**USE platform-specific secure storage:**
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
// lib/services/tokenStorage.js
|
|
11
|
+
exports.TokenStorage = {
|
|
12
|
+
save(token) {
|
|
13
|
+
if (Ti.Platform.osname === 'android') {
|
|
14
|
+
// Use Android KeyStore
|
|
15
|
+
const keyStore = Ti.Android.createKeyStore({
|
|
16
|
+
name: 'SecureKeyStore'
|
|
17
|
+
})
|
|
18
|
+
keyStore.addEntry('authToken', token)
|
|
19
|
+
} else {
|
|
20
|
+
// Use iOS Keychain
|
|
21
|
+
Ti.KeychainItem.setItem({
|
|
22
|
+
identifier: 'authToken',
|
|
23
|
+
value: token,
|
|
24
|
+
accessGroup: 'com.yourapp.keychain'
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
get() {
|
|
30
|
+
if (Ti.Platform.osname === 'android') {
|
|
31
|
+
const keyStore = Ti.Android.createKeyStore({
|
|
32
|
+
name: 'SecureKeyStore'
|
|
33
|
+
})
|
|
34
|
+
return keyStore.getEntry('authToken')
|
|
35
|
+
} else {
|
|
36
|
+
return Ti.KeychainItem.getItem({
|
|
37
|
+
identifier: 'authToken',
|
|
38
|
+
accessGroup: 'com.yourapp.keychain'
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
clear() {
|
|
44
|
+
if (Ti.Platform.osname === 'android') {
|
|
45
|
+
const keyStore = Ti.Android.createKeyStore({
|
|
46
|
+
name: 'SecureKeyStore'
|
|
47
|
+
})
|
|
48
|
+
keyStore.removeEntry('authToken')
|
|
49
|
+
} else {
|
|
50
|
+
Ti.KeychainItem.removeItem({
|
|
51
|
+
identifier: 'authToken',
|
|
52
|
+
accessGroup: 'com.yourapp.keychain'
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Certificate Pinning
|
|
60
|
+
|
|
61
|
+
Prevent man-in-the-middle attacks by pinning SSL certificates:
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
// lib/api/pinnedClient.js
|
|
65
|
+
exports.createPinnedClient = function() {
|
|
66
|
+
const client = Ti.Network.createHTTPClient({
|
|
67
|
+
// Security: Enable certificate pinning
|
|
68
|
+
certificatePinning: true,
|
|
69
|
+
|
|
70
|
+
// Specify allowed certificates
|
|
71
|
+
validatesSecureCertificate: true,
|
|
72
|
+
|
|
73
|
+
onload: () => {
|
|
74
|
+
// Success
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
onerror: (e) => {
|
|
78
|
+
// Certificate validation failed
|
|
79
|
+
if (e.error.indexOf('certificate') >= 0) {
|
|
80
|
+
Ti.API.error('Certificate pinning failed - possible MITM attack')
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
return client
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Add certificates to tiapp.xml:**
|
|
90
|
+
|
|
91
|
+
```xml
|
|
92
|
+
<ti:app>
|
|
93
|
+
<certificates>
|
|
94
|
+
<certificate>
|
|
95
|
+
<name>api.example.com</name>
|
|
96
|
+
<type>rsa</type>
|
|
97
|
+
<file>certificates/api-pin.pem</file>
|
|
98
|
+
</certificate>
|
|
99
|
+
</certificates>
|
|
100
|
+
</ti:app>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Data Encryption at Rest
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
// lib/services/encryption.js
|
|
107
|
+
// AES-256 encryption for sensitive local data
|
|
108
|
+
|
|
109
|
+
const crypto = require('ti.crypto')
|
|
110
|
+
|
|
111
|
+
exports.encrypt = function(data, key) {
|
|
112
|
+
return crypto.encrypt({
|
|
113
|
+
data: data,
|
|
114
|
+
key: key,
|
|
115
|
+
algorithm: crypto.AES_256_CBC,
|
|
116
|
+
options: { mode: crypto.CBC }
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
exports.decrypt = function(encryptedData, key) {
|
|
121
|
+
return crypto.decrypt({
|
|
122
|
+
data: encryptedData,
|
|
123
|
+
key: key,
|
|
124
|
+
algorithm: crypto.AES_256_CBC,
|
|
125
|
+
options: { mode: crypto.CBC }
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Usage: Secure cache of sensitive user data
|
|
130
|
+
module.exports = class SecureCache {
|
|
131
|
+
constructor(encryptionKey) {
|
|
132
|
+
this.key = encryptionKey
|
|
133
|
+
this.cache = {}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
set(key, value) {
|
|
137
|
+
const encrypted = encrypt(JSON.stringify(value), this.key)
|
|
138
|
+
this.cache[key] = encrypted
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get(key) {
|
|
142
|
+
if (!this.cache[key]) return null
|
|
143
|
+
|
|
144
|
+
const decrypted = decrypt(this.cache[key], this.key)
|
|
145
|
+
return JSON.parse(decrypted)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
clear() {
|
|
149
|
+
this.cache = {}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Secure HTTP Communication
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
// lib/api/secureClient.js
|
|
158
|
+
exports.createSecureClient = function(baseUrl) {
|
|
159
|
+
return {
|
|
160
|
+
request(method, endpoint, data = null) {
|
|
161
|
+
return new Promise((resolve, reject) => {
|
|
162
|
+
const client = Ti.Network.createHTTPClient({
|
|
163
|
+
timeout: 10000,
|
|
164
|
+
|
|
165
|
+
onload: function() {
|
|
166
|
+
if (this.status === 200) {
|
|
167
|
+
try {
|
|
168
|
+
resolve(JSON.parse(this.responseText))
|
|
169
|
+
} catch (e) {
|
|
170
|
+
reject(new Error('Invalid JSON response'))
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
reject(new Error(`HTTP ${this.status}`))
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
onerror: function(e) {
|
|
178
|
+
// Log security events
|
|
179
|
+
if (this.status === 401 || this.status === 403) {
|
|
180
|
+
Ti.API.warn(`[SECURITY] Unauthorized: ${endpoint}`)
|
|
181
|
+
}
|
|
182
|
+
reject(e)
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
client.open(method, `${baseUrl}${endpoint}`)
|
|
187
|
+
|
|
188
|
+
// Security headers
|
|
189
|
+
client.setRequestHeader('User-Agent', `MyApp/${Ti.App.version}`)
|
|
190
|
+
client.setRequestHeader('Accept', 'application/json')
|
|
191
|
+
client.setRequestHeader('Content-Type', 'application/json')
|
|
192
|
+
|
|
193
|
+
client.send(data ? JSON.stringify(data) : null)
|
|
194
|
+
})
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
get(endpoint) {
|
|
198
|
+
return this.request('GET', endpoint)
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
post(endpoint, data) {
|
|
202
|
+
return this.request('POST', endpoint, data)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Authentication Token Refresh Pattern
|
|
209
|
+
|
|
210
|
+
```javascript
|
|
211
|
+
// lib/services/authService.js
|
|
212
|
+
const { TokenStorage } = require('lib/services/tokenStorage')
|
|
213
|
+
|
|
214
|
+
const TOKEN_REFRESH_THRESHOLD = 5 * 60 * 1000 // 5 minutes before expiry
|
|
215
|
+
|
|
216
|
+
exports.refreshAuthToken = async function() {
|
|
217
|
+
const refreshToken = TokenStorage.get('refreshToken')
|
|
218
|
+
|
|
219
|
+
const response = await api.post('/auth/refresh', {
|
|
220
|
+
refresh_token: refreshToken
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
TokenStorage.save(response.access_token)
|
|
224
|
+
|
|
225
|
+
// Set up auto-refresh
|
|
226
|
+
scheduleTokenRefresh(response.expires_in)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function scheduleTokenRefresh(expiresIn) {
|
|
230
|
+
const refreshTime = expiresIn - TOKEN_REFRESH_THRESHOLD
|
|
231
|
+
|
|
232
|
+
setTimeout(() => {
|
|
233
|
+
refreshAuthToken().catch(() => {
|
|
234
|
+
// Refresh failed - redirect to login
|
|
235
|
+
Alloy.createController('login').getView().open()
|
|
236
|
+
})
|
|
237
|
+
}, refreshTime)
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Input Validation
|
|
242
|
+
|
|
243
|
+
```javascript
|
|
244
|
+
// lib/services/validator.js
|
|
245
|
+
exports.Validator = {
|
|
246
|
+
email(email) {
|
|
247
|
+
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
248
|
+
if (!regex.test(email)) {
|
|
249
|
+
throw new ValidationError('Invalid email format')
|
|
250
|
+
}
|
|
251
|
+
return email.trim().toLowerCase()
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
password(password) {
|
|
255
|
+
if (password.length < 8) {
|
|
256
|
+
throw new ValidationError('Password must be at least 8 characters')
|
|
257
|
+
}
|
|
258
|
+
// Add more rules as needed
|
|
259
|
+
return password
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
sanitizeInput(input) {
|
|
263
|
+
// Remove potentially dangerous characters
|
|
264
|
+
return input
|
|
265
|
+
.replace(/[<>\"']/g, '')
|
|
266
|
+
.trim()
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## OWASP Mobile Security Checklist
|
|
272
|
+
|
|
273
|
+
| Category | Check | Implementation |
|
|
274
|
+
| -------------------- | --------------------------- | ---------------------------------- |
|
|
275
|
+
| **Data Storage** | Credentials stored securely | Keychain/KeyStore for tokens |
|
|
276
|
+
| **Data Storage** | Sensitive data encrypted | AES-256 for cached data |
|
|
277
|
+
| **Communication** | HTTPS only | `validatesSecureCertificate: true` |
|
|
278
|
+
| **Communication** | Certificate pinning | SSL pinning enabled |
|
|
279
|
+
| **Authentication** | Token refresh | Auto-refresh before expiry |
|
|
280
|
+
| **Authentication** | Session timeout | Auto-logout after inactivity |
|
|
281
|
+
| **Input Validation** | Server-side validation | Never trust client input |
|
|
282
|
+
| **Input Validation** | Sanitize user input | Remove XSS patterns |
|
|
283
|
+
| **Cryptography** | No hardcoded keys | Keys from secure storage |
|
|
284
|
+
| **Cryptography** | Use standard algorithms | AES-256, SHA-256 |
|
|
285
|
+
|
|
286
|
+
## Biometric Authentication
|
|
287
|
+
|
|
288
|
+
### Using ti.identity Module
|
|
289
|
+
|
|
290
|
+
```javascript
|
|
291
|
+
// lib/services/biometricService.js
|
|
292
|
+
const Identity = require('ti.identity')
|
|
293
|
+
|
|
294
|
+
exports.BiometricService = {
|
|
295
|
+
/**
|
|
296
|
+
* Check if biometrics are available
|
|
297
|
+
* @returns {{available: boolean, type: string|null, error: string|null}}
|
|
298
|
+
*/
|
|
299
|
+
checkAvailability() {
|
|
300
|
+
if (!Identity.isSupported()) {
|
|
301
|
+
return { available: false, type: null, error: 'Biometrics not supported' }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const authResult = Identity.deviceCanAuthenticate()
|
|
305
|
+
|
|
306
|
+
if (authResult !== Identity.SUCCESS) {
|
|
307
|
+
const errors = {
|
|
308
|
+
[Identity.ERROR_TOUCH_ID_NOT_AVAILABLE]: 'Biometrics not available',
|
|
309
|
+
[Identity.ERROR_TOUCH_ID_NOT_ENROLLED]: 'No biometrics enrolled',
|
|
310
|
+
[Identity.ERROR_PASSCODE_NOT_SET]: 'Device passcode not set'
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
available: false,
|
|
315
|
+
type: null,
|
|
316
|
+
error: errors[authResult] || 'Unknown error'
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Determine biometric type
|
|
321
|
+
const biometricType = OS_IOS
|
|
322
|
+
? (Identity.biometryType === Identity.BIOMETRY_TYPE_FACE_ID ? 'Face ID' : 'Touch ID')
|
|
323
|
+
: 'Fingerprint'
|
|
324
|
+
|
|
325
|
+
return { available: true, type: biometricType, error: null }
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Authenticate user with biometrics
|
|
330
|
+
* @param {string} reason - Reason shown to user
|
|
331
|
+
* @returns {Promise<boolean>}
|
|
332
|
+
*/
|
|
333
|
+
authenticate(reason = L('biometric_reason')) {
|
|
334
|
+
return new Promise((resolve, reject) => {
|
|
335
|
+
const { available, error } = this.checkAvailability()
|
|
336
|
+
|
|
337
|
+
if (!available) {
|
|
338
|
+
return reject(new Error(error))
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
Identity.authenticate({
|
|
342
|
+
reason: reason,
|
|
343
|
+
allowableReuseDuration: 0, // Always require fresh auth
|
|
344
|
+
fallbackTitle: L('use_passcode'), // iOS fallback button
|
|
345
|
+
cancelTitle: L('cancel'),
|
|
346
|
+
|
|
347
|
+
callback: (e) => {
|
|
348
|
+
if (e.success) {
|
|
349
|
+
resolve(true)
|
|
350
|
+
} else {
|
|
351
|
+
const errorMsg = e.error || 'Authentication failed'
|
|
352
|
+
reject(new Error(errorMsg))
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
})
|
|
356
|
+
})
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Get user-friendly name for the biometric type
|
|
361
|
+
*/
|
|
362
|
+
getBiometricName() {
|
|
363
|
+
const { available, type } = this.checkAvailability()
|
|
364
|
+
return available ? type : null
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Biometric Login Flow
|
|
370
|
+
|
|
371
|
+
```javascript
|
|
372
|
+
// controllers/auth/login.js
|
|
373
|
+
const { BiometricService } = require('lib/services/biometricService')
|
|
374
|
+
const { TokenStorage } = require('lib/services/tokenStorage')
|
|
375
|
+
const { AuthService } = require('lib/services/authService')
|
|
376
|
+
|
|
377
|
+
function init() {
|
|
378
|
+
// Check if biometric login is available and enabled
|
|
379
|
+
const { available, type } = BiometricService.checkAvailability()
|
|
380
|
+
const biometricEnabled = Ti.App.Properties.getBool('biometricEnabled', false)
|
|
381
|
+
const hasStoredCredentials = TokenStorage.hasRefreshToken()
|
|
382
|
+
|
|
383
|
+
if (available && biometricEnabled && hasStoredCredentials) {
|
|
384
|
+
// Show biometric login option
|
|
385
|
+
$.biometricBtn.applyProperties({
|
|
386
|
+
visible: true,
|
|
387
|
+
title: String.format(L('login_with'), type)
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function onBiometricLogin() {
|
|
393
|
+
try {
|
|
394
|
+
// Authenticate with biometrics
|
|
395
|
+
await BiometricService.authenticate(L('unlock_app'))
|
|
396
|
+
|
|
397
|
+
// Refresh token using stored refresh token
|
|
398
|
+
const user = await AuthService.refreshSession()
|
|
399
|
+
|
|
400
|
+
// Navigate to main app
|
|
401
|
+
Navigation.replace('main')
|
|
402
|
+
|
|
403
|
+
} catch (error) {
|
|
404
|
+
// Biometric failed - show password login
|
|
405
|
+
showMessage(L('biometric_failed'))
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Enable biometrics after successful password login
|
|
410
|
+
async function onLoginSuccess(user) {
|
|
411
|
+
const { available, type } = BiometricService.checkAvailability()
|
|
412
|
+
|
|
413
|
+
if (available && !Ti.App.Properties.getBool('askedBiometric', false)) {
|
|
414
|
+
Ti.App.Properties.setBool('askedBiometric', true)
|
|
415
|
+
|
|
416
|
+
// Ask user if they want to enable biometric login
|
|
417
|
+
const dialog = Ti.UI.createAlertDialog({
|
|
418
|
+
title: String.format(L('enable_biometric_title'), type),
|
|
419
|
+
message: String.format(L('enable_biometric_msg'), type),
|
|
420
|
+
buttonNames: [L('not_now'), L('enable')]
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
dialog.addEventListener('click', (e) => {
|
|
424
|
+
if (e.index === 1) {
|
|
425
|
+
Ti.App.Properties.setBool('biometricEnabled', true)
|
|
426
|
+
}
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
dialog.show()
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## Deep Link Security
|
|
435
|
+
|
|
436
|
+
### Validating Deep Links
|
|
437
|
+
|
|
438
|
+
```javascript
|
|
439
|
+
// lib/services/deepLinkService.js
|
|
440
|
+
const logger = require('lib/services/logger')
|
|
441
|
+
|
|
442
|
+
// Whitelist of allowed schemes and hosts
|
|
443
|
+
const ALLOWED_SCHEMES = ['myapp', 'https']
|
|
444
|
+
const ALLOWED_HOSTS = ['myapp.com', 'www.myapp.com']
|
|
445
|
+
|
|
446
|
+
// Route patterns with their required auth levels
|
|
447
|
+
const ROUTES = {
|
|
448
|
+
'/product/:id': { auth: false, handler: 'openProduct' },
|
|
449
|
+
'/order/:id': { auth: true, handler: 'openOrder' },
|
|
450
|
+
'/profile': { auth: true, handler: 'openProfile' },
|
|
451
|
+
'/verify-email': { auth: false, handler: 'verifyEmail' }
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
exports.DeepLinkService = {
|
|
455
|
+
/**
|
|
456
|
+
* Parse and validate a deep link URL
|
|
457
|
+
* @param {string} url - The deep link URL
|
|
458
|
+
* @returns {{valid: boolean, route: string, params: object, error: string}}
|
|
459
|
+
*/
|
|
460
|
+
parseUrl(url) {
|
|
461
|
+
try {
|
|
462
|
+
const parsed = this._parseUrlComponents(url)
|
|
463
|
+
|
|
464
|
+
// Validate scheme
|
|
465
|
+
if (!ALLOWED_SCHEMES.includes(parsed.scheme)) {
|
|
466
|
+
logger.warn('DeepLink', 'Invalid scheme', { url, scheme: parsed.scheme })
|
|
467
|
+
return { valid: false, error: 'Invalid URL scheme' }
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Validate host for https URLs
|
|
471
|
+
if (parsed.scheme === 'https' && !ALLOWED_HOSTS.includes(parsed.host)) {
|
|
472
|
+
logger.warn('DeepLink', 'Invalid host', { url, host: parsed.host })
|
|
473
|
+
return { valid: false, error: 'Invalid host' }
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Match route
|
|
477
|
+
const routeMatch = this._matchRoute(parsed.path)
|
|
478
|
+
|
|
479
|
+
if (!routeMatch) {
|
|
480
|
+
logger.warn('DeepLink', 'Unknown route', { url, path: parsed.path })
|
|
481
|
+
return { valid: false, error: 'Unknown route' }
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Sanitize parameters
|
|
485
|
+
const params = this._sanitizeParams({
|
|
486
|
+
...routeMatch.params,
|
|
487
|
+
...parsed.queryParams
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
valid: true,
|
|
492
|
+
route: routeMatch.route,
|
|
493
|
+
handler: routeMatch.handler,
|
|
494
|
+
requiresAuth: routeMatch.requiresAuth,
|
|
495
|
+
params
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
} catch (error) {
|
|
499
|
+
logger.error('DeepLink', 'Parse error', { url, error: error.message })
|
|
500
|
+
return { valid: false, error: 'Invalid URL format' }
|
|
501
|
+
}
|
|
502
|
+
},
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Handle an incoming deep link
|
|
506
|
+
*/
|
|
507
|
+
async handle(url) {
|
|
508
|
+
const result = this.parseUrl(url)
|
|
509
|
+
|
|
510
|
+
if (!result.valid) {
|
|
511
|
+
return false
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Check auth requirement
|
|
515
|
+
if (result.requiresAuth && !AuthService.isAuthenticated()) {
|
|
516
|
+
// Store deep link for after login
|
|
517
|
+
Ti.App.Properties.setString('pendingDeepLink', url)
|
|
518
|
+
Navigation.open('login')
|
|
519
|
+
return true
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Execute handler
|
|
523
|
+
return this._executeHandler(result.handler, result.params)
|
|
524
|
+
},
|
|
525
|
+
|
|
526
|
+
_parseUrlComponents(url) {
|
|
527
|
+
// Custom parsing to handle both custom schemes and https
|
|
528
|
+
const schemeMatch = url.match(/^([a-z]+):\/\//)
|
|
529
|
+
const scheme = schemeMatch ? schemeMatch[1] : 'https'
|
|
530
|
+
|
|
531
|
+
const withoutScheme = url.replace(/^[a-z]+:\/\//, '')
|
|
532
|
+
const [hostPath, queryString] = withoutScheme.split('?')
|
|
533
|
+
const [host, ...pathParts] = hostPath.split('/')
|
|
534
|
+
const path = '/' + pathParts.join('/')
|
|
535
|
+
|
|
536
|
+
const queryParams = {}
|
|
537
|
+
if (queryString) {
|
|
538
|
+
queryString.split('&').forEach(pair => {
|
|
539
|
+
const [key, value] = pair.split('=')
|
|
540
|
+
queryParams[decodeURIComponent(key)] = decodeURIComponent(value || '')
|
|
541
|
+
})
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return { scheme, host, path, queryParams }
|
|
545
|
+
},
|
|
546
|
+
|
|
547
|
+
_matchRoute(path) {
|
|
548
|
+
for (const [pattern, config] of Object.entries(ROUTES)) {
|
|
549
|
+
const params = this._extractParams(pattern, path)
|
|
550
|
+
if (params) {
|
|
551
|
+
return {
|
|
552
|
+
route: pattern,
|
|
553
|
+
handler: config.handler,
|
|
554
|
+
requiresAuth: config.auth,
|
|
555
|
+
params
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return null
|
|
560
|
+
},
|
|
561
|
+
|
|
562
|
+
_extractParams(pattern, path) {
|
|
563
|
+
const patternParts = pattern.split('/')
|
|
564
|
+
const pathParts = path.split('/')
|
|
565
|
+
|
|
566
|
+
if (patternParts.length !== pathParts.length) return null
|
|
567
|
+
|
|
568
|
+
const params = {}
|
|
569
|
+
|
|
570
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
571
|
+
if (patternParts[i].startsWith(':')) {
|
|
572
|
+
const paramName = patternParts[i].slice(1)
|
|
573
|
+
params[paramName] = pathParts[i]
|
|
574
|
+
} else if (patternParts[i] !== pathParts[i]) {
|
|
575
|
+
return null
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return params
|
|
580
|
+
},
|
|
581
|
+
|
|
582
|
+
_sanitizeParams(params) {
|
|
583
|
+
const sanitized = {}
|
|
584
|
+
|
|
585
|
+
for (const [key, value] of Object.entries(params)) {
|
|
586
|
+
// Remove potentially dangerous characters
|
|
587
|
+
const cleanKey = key.replace(/[<>"'&]/g, '')
|
|
588
|
+
const cleanValue = String(value).replace(/[<>"'&]/g, '').slice(0, 1000)
|
|
589
|
+
sanitized[cleanKey] = cleanValue
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return sanitized
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### Registering Deep Link Handler
|
|
598
|
+
|
|
599
|
+
```javascript
|
|
600
|
+
// alloy.js
|
|
601
|
+
const { DeepLinkService } = require('lib/services/deepLinkService')
|
|
602
|
+
|
|
603
|
+
// iOS: Handle app launch from deep link
|
|
604
|
+
if (Ti.App.getArguments().url) {
|
|
605
|
+
DeepLinkService.handle(Ti.App.getArguments().url)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Handle deep links while app is running
|
|
609
|
+
Ti.App.addEventListener('resumed', (e) => {
|
|
610
|
+
if (e.url) {
|
|
611
|
+
DeepLinkService.handle(e.url)
|
|
612
|
+
}
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
// Android: Handle intent
|
|
616
|
+
if (OS_ANDROID) {
|
|
617
|
+
const activity = Ti.Android.currentActivity
|
|
618
|
+
const intent = activity.intent
|
|
619
|
+
|
|
620
|
+
if (intent && intent.data) {
|
|
621
|
+
DeepLinkService.handle(intent.data)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
activity.addEventListener('newintent', (e) => {
|
|
625
|
+
if (e.intent && e.intent.data) {
|
|
626
|
+
DeepLinkService.handle(e.intent.data)
|
|
627
|
+
}
|
|
628
|
+
})
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
## Jailbreak/Root Detection
|
|
633
|
+
|
|
634
|
+
### Detection Service
|
|
635
|
+
|
|
636
|
+
```javascript
|
|
637
|
+
// lib/services/securityService.js
|
|
638
|
+
const logger = require('lib/services/logger')
|
|
639
|
+
|
|
640
|
+
exports.SecurityService = {
|
|
641
|
+
/**
|
|
642
|
+
* Check if device is jailbroken (iOS) or rooted (Android)
|
|
643
|
+
* @returns {{compromised: boolean, reasons: string[]}}
|
|
644
|
+
*/
|
|
645
|
+
checkDeviceIntegrity() {
|
|
646
|
+
const reasons = []
|
|
647
|
+
|
|
648
|
+
if (OS_IOS) {
|
|
649
|
+
reasons.push(...this._checkiOSJailbreak())
|
|
650
|
+
} else {
|
|
651
|
+
reasons.push(...this._checkAndroidRoot())
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (reasons.length > 0) {
|
|
655
|
+
logger.warn('Security', 'Device integrity check failed', { reasons })
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return {
|
|
659
|
+
compromised: reasons.length > 0,
|
|
660
|
+
reasons
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
|
|
664
|
+
_checkiOSJailbreak() {
|
|
665
|
+
const reasons = []
|
|
666
|
+
const file = Ti.Filesystem.getFile
|
|
667
|
+
|
|
668
|
+
// Check for common jailbreak files
|
|
669
|
+
const jailbreakPaths = [
|
|
670
|
+
'/Applications/Cydia.app',
|
|
671
|
+
'/Library/MobileSubstrate/MobileSubstrate.dylib',
|
|
672
|
+
'/bin/bash',
|
|
673
|
+
'/usr/sbin/sshd',
|
|
674
|
+
'/etc/apt',
|
|
675
|
+
'/private/var/lib/apt/',
|
|
676
|
+
'/usr/bin/ssh'
|
|
677
|
+
]
|
|
678
|
+
|
|
679
|
+
jailbreakPaths.forEach(path => {
|
|
680
|
+
try {
|
|
681
|
+
if (file(path).exists()) {
|
|
682
|
+
reasons.push(`Jailbreak file found: ${path}`)
|
|
683
|
+
}
|
|
684
|
+
} catch (e) {
|
|
685
|
+
// File access error might indicate sandbox bypass attempt
|
|
686
|
+
}
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
// Check if we can write outside sandbox
|
|
690
|
+
try {
|
|
691
|
+
const testFile = file('/private/jailbreak_test')
|
|
692
|
+
testFile.write('test')
|
|
693
|
+
testFile.deleteFile()
|
|
694
|
+
reasons.push('Sandbox bypass detected')
|
|
695
|
+
} catch (e) {
|
|
696
|
+
// Expected - sandbox is working
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return reasons
|
|
700
|
+
},
|
|
701
|
+
|
|
702
|
+
_checkAndroidRoot() {
|
|
703
|
+
const reasons = []
|
|
704
|
+
const file = Ti.Filesystem.getFile
|
|
705
|
+
|
|
706
|
+
// Check for su binary
|
|
707
|
+
const suPaths = [
|
|
708
|
+
'/system/app/Superuser.apk',
|
|
709
|
+
'/sbin/su',
|
|
710
|
+
'/system/bin/su',
|
|
711
|
+
'/system/xbin/su',
|
|
712
|
+
'/data/local/xbin/su',
|
|
713
|
+
'/data/local/bin/su',
|
|
714
|
+
'/system/sd/xbin/su',
|
|
715
|
+
'/system/bin/failsafe/su',
|
|
716
|
+
'/data/local/su'
|
|
717
|
+
]
|
|
718
|
+
|
|
719
|
+
suPaths.forEach(path => {
|
|
720
|
+
try {
|
|
721
|
+
if (file(path).exists()) {
|
|
722
|
+
reasons.push(`Root binary found: ${path}`)
|
|
723
|
+
}
|
|
724
|
+
} catch (e) {
|
|
725
|
+
// Ignore access errors
|
|
726
|
+
}
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
// Check for root management apps
|
|
730
|
+
const rootApps = [
|
|
731
|
+
'com.topjohnwu.magisk',
|
|
732
|
+
'com.koushikdutta.superuser',
|
|
733
|
+
'com.noshufou.android.su',
|
|
734
|
+
'eu.chainfire.supersu'
|
|
735
|
+
]
|
|
736
|
+
|
|
737
|
+
// Note: Checking installed packages requires additional permissions
|
|
738
|
+
// This is a simplified check
|
|
739
|
+
|
|
740
|
+
// Check build tags
|
|
741
|
+
const buildTags = Ti.Platform.model
|
|
742
|
+
if (buildTags && buildTags.includes('test-keys')) {
|
|
743
|
+
reasons.push('Test build detected')
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return reasons
|
|
747
|
+
},
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Enforce security policy based on device integrity
|
|
751
|
+
*/
|
|
752
|
+
enforceSecurityPolicy() {
|
|
753
|
+
const { compromised, reasons } = this.checkDeviceIntegrity()
|
|
754
|
+
|
|
755
|
+
if (!compromised) return true
|
|
756
|
+
|
|
757
|
+
// Log security event
|
|
758
|
+
logger.error('Security', 'Compromised device detected', {
|
|
759
|
+
reasons,
|
|
760
|
+
platform: Ti.Platform.osname,
|
|
761
|
+
model: Ti.Platform.model
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
// Get configured policy
|
|
765
|
+
const policy = Alloy.CFG.securityPolicy || 'warn'
|
|
766
|
+
|
|
767
|
+
switch (policy) {
|
|
768
|
+
case 'block':
|
|
769
|
+
// Prevent app usage
|
|
770
|
+
this._showBlockedScreen()
|
|
771
|
+
return false
|
|
772
|
+
|
|
773
|
+
case 'restrict':
|
|
774
|
+
// Disable sensitive features
|
|
775
|
+
Ti.App.Properties.setBool('restrictedMode', true)
|
|
776
|
+
this._showWarning()
|
|
777
|
+
return true
|
|
778
|
+
|
|
779
|
+
case 'warn':
|
|
780
|
+
default:
|
|
781
|
+
// Just warn the user
|
|
782
|
+
this._showWarning()
|
|
783
|
+
return true
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
|
|
787
|
+
_showBlockedScreen() {
|
|
788
|
+
const dialog = Ti.UI.createAlertDialog({
|
|
789
|
+
title: L('security_blocked_title'),
|
|
790
|
+
message: L('security_blocked_msg'),
|
|
791
|
+
buttonNames: [L('close')]
|
|
792
|
+
})
|
|
793
|
+
|
|
794
|
+
dialog.addEventListener('click', () => {
|
|
795
|
+
// Close the app (iOS) or minimize (Android)
|
|
796
|
+
if (OS_IOS) {
|
|
797
|
+
Ti.Platform.openURL('prefs:root=General')
|
|
798
|
+
}
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
dialog.show()
|
|
802
|
+
},
|
|
803
|
+
|
|
804
|
+
_showWarning() {
|
|
805
|
+
Ti.UI.createAlertDialog({
|
|
806
|
+
title: L('security_warning_title'),
|
|
807
|
+
message: L('security_warning_msg'),
|
|
808
|
+
buttonNames: [L('i_understand')]
|
|
809
|
+
}).show()
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
### Integrating Security Checks
|
|
815
|
+
|
|
816
|
+
```javascript
|
|
817
|
+
// alloy.js
|
|
818
|
+
const { SecurityService } = require('lib/services/securityService')
|
|
819
|
+
|
|
820
|
+
// Check device integrity at app start
|
|
821
|
+
const securityCheck = SecurityService.enforceSecurityPolicy()
|
|
822
|
+
|
|
823
|
+
if (!securityCheck) {
|
|
824
|
+
// Don't initialize the app
|
|
825
|
+
return
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Check again periodically (in case of runtime jailbreak tools)
|
|
829
|
+
setInterval(() => {
|
|
830
|
+
SecurityService.checkDeviceIntegrity()
|
|
831
|
+
}, 5 * 60 * 1000) // Every 5 minutes
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
## Additional Security Checklist
|
|
835
|
+
|
|
836
|
+
| Category | Check | Implementation |
|
|
837
|
+
| -------------- | -------------------------- | ------------------------ |
|
|
838
|
+
| **Biometrics** | Use ti.identity for auth | BiometricService wrapper |
|
|
839
|
+
| **Biometrics** | Never store biometric data | System handles storage |
|
|
840
|
+
| **Biometrics** | Fallback to password | Always offer alternative |
|
|
841
|
+
| **Deep Links** | Whitelist allowed schemes | ALLOWED_SCHEMES constant |
|
|
842
|
+
| **Deep Links** | Whitelist allowed hosts | ALLOWED_HOSTS constant |
|
|
843
|
+
| **Deep Links** | Sanitize all parameters | _sanitizeParams() |
|
|
844
|
+
| **Deep Links** | Check auth requirements | requiresAuth per route |
|
|
845
|
+
| **Integrity** | Check for jailbreak/root | checkDeviceIntegrity() |
|
|
846
|
+
| **Integrity** | Define security policy | block/restrict/warn |
|
|
847
|
+
| **Integrity** | Log security events | Always log compromises |
|