@capgo/capacitor-device-integrity 8.0.2
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/CapgoCapacitorDeviceIntegrity.podspec +17 -0
- package/LICENSE +373 -0
- package/Package.swift +28 -0
- package/README.md +346 -0
- package/android/build.gradle +61 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/app/capgo/deviceintegrity/DeviceIntegrity.java +276 -0
- package/android/src/main/java/app/capgo/deviceintegrity/DeviceIntegrityPlugin.java +260 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +495 -0
- package/dist/esm/definitions.d.ts +218 -0
- package/dist/esm/definitions.js +3 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +10 -0
- package/dist/esm/web.js +37 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +47 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +50 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/DeviceIntegrityPlugin/DeviceIntegrity.swift +49 -0
- package/ios/Sources/DeviceIntegrityPlugin/DeviceIntegrityPlugin.swift +208 -0
- package/ios/Tests/DeviceIntegrityPluginTests/DeviceIntegrityTests.swift +24 -0
- package/package.json +92 -0
package/README.md
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# @capgo/capacitor-device-integrity
|
|
2
|
+
|
|
3
|
+
<a href="https://capgo.app/"><img src="https://capgo.app/readme-banner.svg?repo=Cap-go/capacitor-device-integrity" alt="Capgo - Instant updates for Capacitor" /></a>
|
|
4
|
+
|
|
5
|
+
<div align="center">
|
|
6
|
+
<h2><a href="https://capgo.app/?ref=plugin_device_integrity">Get Instant updates for your App with Capgo</a></h2>
|
|
7
|
+
<h2><a href="https://capgo.app/consulting/?ref=plugin_device_integrity">Missing a feature? We will build the plugin for you</a></h2>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
Device integrity and fraud signals for Capacitor:
|
|
11
|
+
|
|
12
|
+
- Android: Widevine DRM fingerprint and Play Integrity Standard API attestation.
|
|
13
|
+
- iOS: Apple App Attest and DeviceCheck.
|
|
14
|
+
- Web: unsupported fallback that reports no native capabilities.
|
|
15
|
+
|
|
16
|
+
## Security model
|
|
17
|
+
|
|
18
|
+
This plugin is designed for fraud and abuse detection. It does not make client-side values trustworthy by itself.
|
|
19
|
+
|
|
20
|
+
- Verify App Attest and Play Integrity tokens on your backend.
|
|
21
|
+
- Treat Widevine values as sensitive identifiers.
|
|
22
|
+
- Disclose identifier use in your privacy policy.
|
|
23
|
+
- Do not use Android Widevine values for advertising or cross-app tracking.
|
|
24
|
+
- Do not attempt iOS fingerprinting. Apple does not provide a public review-safe stable device ID for this use case.
|
|
25
|
+
|
|
26
|
+
## Compatibility
|
|
27
|
+
|
|
28
|
+
| Plugin version | Capacitor compatibility | Maintained |
|
|
29
|
+
| -------------- | ----------------------- | ---------- |
|
|
30
|
+
| v8.*.* | v8.*.* | Yes |
|
|
31
|
+
| v7.*.* | v7.*.* | On demand |
|
|
32
|
+
| v6.*.* | v6.*.* | On demand |
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install @capgo/capacitor-device-integrity
|
|
38
|
+
npx cap sync
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Platform setup
|
|
42
|
+
|
|
43
|
+
### Android
|
|
44
|
+
|
|
45
|
+
Widevine fingerprinting does not require extra permissions.
|
|
46
|
+
|
|
47
|
+
Play Integrity requires Google Play services and a Cloud project number:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// capacitor.config.ts
|
|
51
|
+
plugins: {
|
|
52
|
+
DeviceIntegrity: {
|
|
53
|
+
cloudProjectNumber: '123456789012'
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
You can also pass `cloudProjectNumber` directly to the attestation methods.
|
|
59
|
+
|
|
60
|
+
### iOS
|
|
61
|
+
|
|
62
|
+
Enable the App Attest capability in Xcode:
|
|
63
|
+
|
|
64
|
+
1. Open your app target.
|
|
65
|
+
2. Go to Signing & Capabilities.
|
|
66
|
+
3. Add App Attest.
|
|
67
|
+
4. Test on a physical device when validating real App Attest and DeviceCheck behavior.
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { DeviceIntegrity } from '@capgo/capacitor-device-integrity';
|
|
73
|
+
|
|
74
|
+
const capabilities = await DeviceIntegrity.getCapabilities();
|
|
75
|
+
|
|
76
|
+
if (capabilities.platform === 'android' && capabilities.widevine.fingerprintAvailable) {
|
|
77
|
+
const widevine = await DeviceIntegrity.getWidevineFingerprint();
|
|
78
|
+
await api.saveDeviceSignal({
|
|
79
|
+
widevineIdSha256: widevine.widevineIdSha256,
|
|
80
|
+
fingerprint: widevine.fingerprint,
|
|
81
|
+
securityLevel: widevine.securityLevel,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (capabilities.appAttest.supported || capabilities.playIntegrity.supported) {
|
|
86
|
+
const prepared = await DeviceIntegrity.prepareAttestation();
|
|
87
|
+
const challenge = await api.createDeviceChallenge();
|
|
88
|
+
|
|
89
|
+
const attestation = await DeviceIntegrity.createAttestation({
|
|
90
|
+
keyId: prepared.keyId,
|
|
91
|
+
challenge,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await api.verifyDeviceAttestation(attestation);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (capabilities.deviceCheck.supported) {
|
|
98
|
+
const deviceCheck = await DeviceIntegrity.getDeviceCheckToken();
|
|
99
|
+
await api.verifyDeviceCheckToken(deviceCheck.token);
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Backend handling
|
|
104
|
+
|
|
105
|
+
Store the platform-specific evidence with the user record only after server-side checks:
|
|
106
|
+
|
|
107
|
+
- Android Widevine: store `widevineIdSha256` and optionally the raw `widevineIdBase64` only when you explicitly need it.
|
|
108
|
+
- Android Play Integrity: decode the returned token with Google's server API and validate request hash, package name, certificate digest, and device/app integrity verdicts.
|
|
109
|
+
- iOS App Attest: verify the attestation object/assertion against Apple's App Attest rules, app identity, nonce, public key, and counter.
|
|
110
|
+
- iOS DeviceCheck: send the token to Apple's DeviceCheck server API and maintain fraud bits on your backend.
|
|
111
|
+
|
|
112
|
+
## API
|
|
113
|
+
|
|
114
|
+
<docgen-index>
|
|
115
|
+
|
|
116
|
+
* [`getCapabilities()`](#getcapabilities)
|
|
117
|
+
* [`getWidevineFingerprint(...)`](#getwidevinefingerprint)
|
|
118
|
+
* [`prepareAttestation(...)`](#prepareattestation)
|
|
119
|
+
* [`createAttestation(...)`](#createattestation)
|
|
120
|
+
* [`createAssertion(...)`](#createassertion)
|
|
121
|
+
* [`getDeviceCheckToken()`](#getdevicechecktoken)
|
|
122
|
+
* [Interfaces](#interfaces)
|
|
123
|
+
* [Type Aliases](#type-aliases)
|
|
124
|
+
|
|
125
|
+
</docgen-index>
|
|
126
|
+
|
|
127
|
+
<docgen-api>
|
|
128
|
+
<!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
|
|
129
|
+
|
|
130
|
+
### getCapabilities()
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
getCapabilities() => Promise<DeviceIntegrityCapabilities>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Returns the native integrity capabilities available on the current platform.
|
|
137
|
+
|
|
138
|
+
**Returns:** <code>Promise<<a href="#deviceintegritycapabilities">DeviceIntegrityCapabilities</a>></code>
|
|
139
|
+
|
|
140
|
+
--------------------
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
### getWidevineFingerprint(...)
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
getWidevineFingerprint(options?: WidevineFingerprintOptions | undefined) => Promise<WidevineFingerprintResult>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Returns an Android Widevine-derived fingerprint.
|
|
150
|
+
|
|
151
|
+
The default `fingerprint` is SHA-256 over the Widevine device unique ID and a salt.
|
|
152
|
+
If `hashSalt` is not provided, Android uses the app package name as the salt.
|
|
153
|
+
|
|
154
|
+
The raw Widevine ID is sensitive and is only returned as base64 when `includeRawId` is true.
|
|
155
|
+
|
|
156
|
+
| Param | Type |
|
|
157
|
+
| ------------- | --------------------------------------------------------------------------------- |
|
|
158
|
+
| **`options`** | <code><a href="#widevinefingerprintoptions">WidevineFingerprintOptions</a></code> |
|
|
159
|
+
|
|
160
|
+
**Returns:** <code>Promise<<a href="#widevinefingerprintresult">WidevineFingerprintResult</a>></code>
|
|
161
|
+
|
|
162
|
+
--------------------
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
### prepareAttestation(...)
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
prepareAttestation(options?: PrepareAttestationOptions | undefined) => Promise<PrepareAttestationResult>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Prepares native attestation state and returns the key/provider handle.
|
|
172
|
+
|
|
173
|
+
iOS: creates an App Attest key.
|
|
174
|
+
Android: prepares a Play Integrity Standard token provider.
|
|
175
|
+
|
|
176
|
+
| Param | Type |
|
|
177
|
+
| ------------- | ------------------------------------------------------------------------------- |
|
|
178
|
+
| **`options`** | <code><a href="#prepareattestationoptions">PrepareAttestationOptions</a></code> |
|
|
179
|
+
|
|
180
|
+
**Returns:** <code>Promise<<a href="#prepareattestationresult">PrepareAttestationResult</a>></code>
|
|
181
|
+
|
|
182
|
+
--------------------
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
### createAttestation(...)
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
createAttestation(options: CreateAttestationOptions) => Promise<AttestationTokenResult>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Creates a registration attestation token bound to a backend-issued challenge.
|
|
192
|
+
|
|
193
|
+
| Param | Type |
|
|
194
|
+
| ------------- | ----------------------------------------------------------------------------- |
|
|
195
|
+
| **`options`** | <code><a href="#createattestationoptions">CreateAttestationOptions</a></code> |
|
|
196
|
+
|
|
197
|
+
**Returns:** <code>Promise<<a href="#attestationtokenresult">AttestationTokenResult</a>></code>
|
|
198
|
+
|
|
199
|
+
--------------------
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
### createAssertion(...)
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
createAssertion(options: CreateAssertionOptions) => Promise<AttestationTokenResult>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Creates a request assertion token bound to a request payload.
|
|
209
|
+
|
|
210
|
+
| Param | Type |
|
|
211
|
+
| ------------- | ------------------------------------------------------------------------- |
|
|
212
|
+
| **`options`** | <code><a href="#createassertionoptions">CreateAssertionOptions</a></code> |
|
|
213
|
+
|
|
214
|
+
**Returns:** <code>Promise<<a href="#attestationtokenresult">AttestationTokenResult</a>></code>
|
|
215
|
+
|
|
216
|
+
--------------------
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
### getDeviceCheckToken()
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
getDeviceCheckToken() => Promise<DeviceCheckTokenResult>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Creates an iOS DeviceCheck token for server-side fraud-state lookups.
|
|
226
|
+
|
|
227
|
+
**Returns:** <code>Promise<<a href="#devicechecktokenresult">DeviceCheckTokenResult</a>></code>
|
|
228
|
+
|
|
229
|
+
--------------------
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
### Interfaces
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
#### DeviceIntegrityCapabilities
|
|
236
|
+
|
|
237
|
+
| Prop | Type | Description |
|
|
238
|
+
| ------------------- | --------------------------------------------------------------------------- | ---------------------------------------- |
|
|
239
|
+
| **`platform`** | <code><a href="#deviceintegrityplatform">DeviceIntegrityPlatform</a></code> | Platform currently executing the plugin. |
|
|
240
|
+
| **`widevine`** | <code><a href="#widevinecapabilities">WidevineCapabilities</a></code> | Android Widevine DRM support. |
|
|
241
|
+
| **`appAttest`** | <code><a href="#supportstatus">SupportStatus</a></code> | iOS App Attest support. |
|
|
242
|
+
| **`deviceCheck`** | <code><a href="#supportstatus">SupportStatus</a></code> | iOS DeviceCheck support. |
|
|
243
|
+
| **`playIntegrity`** | <code><a href="#supportstatus">SupportStatus</a></code> | Android Play Integrity support. |
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
#### WidevineCapabilities
|
|
247
|
+
|
|
248
|
+
| Prop | Type | Description |
|
|
249
|
+
| -------------------------------- | -------------------- | ----------------------------------------------------------- |
|
|
250
|
+
| **`supported`** | <code>boolean</code> | Whether the Widevine DRM scheme is supported by the device. |
|
|
251
|
+
| **`fingerprintAvailable`** | <code>boolean</code> | Whether the Widevine device unique ID can be read. |
|
|
252
|
+
| **`securityLevelScanSupported`** | <code>boolean</code> | Whether the Widevine security level property can be read. |
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
#### SupportStatus
|
|
256
|
+
|
|
257
|
+
| Prop | Type | Description |
|
|
258
|
+
| --------------- | -------------------- | ---------------------------------------------------------- |
|
|
259
|
+
| **`supported`** | <code>boolean</code> | Whether the capability is available on the current device. |
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
#### WidevineFingerprintResult
|
|
263
|
+
|
|
264
|
+
| Prop | Type | Description |
|
|
265
|
+
| ---------------------- | ----------------------- | ------------------------------------------------------------------------------------------- |
|
|
266
|
+
| **`platform`** | <code>'android'</code> | Always `android`. |
|
|
267
|
+
| **`source`** | <code>'widevine'</code> | Always `widevine`. |
|
|
268
|
+
| **`fingerprint`** | <code>string</code> | Salted SHA-256 fingerprint for storing alongside a user record. |
|
|
269
|
+
| **`widevineIdSha256`** | <code>string</code> | Unsalted SHA-256 hash of the Widevine device unique ID. |
|
|
270
|
+
| **`widevineIdBase64`** | <code>string</code> | Raw Widevine device unique ID encoded as base64. Returned only when `includeRawId` is true. |
|
|
271
|
+
| **`securityLevel`** | <code>string</code> | Widevine security level when available, for example `L1` or `L3`. |
|
|
272
|
+
| **`vendor`** | <code>string</code> | DRM vendor when available. |
|
|
273
|
+
| **`version`** | <code>string</code> | DRM plugin version when available. |
|
|
274
|
+
| **`description`** | <code>string</code> | DRM plugin description when available. |
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
#### WidevineFingerprintOptions
|
|
278
|
+
|
|
279
|
+
| Prop | Type | Description |
|
|
280
|
+
| ------------------ | -------------------- | ------------------------------------------------------------------------------------------- |
|
|
281
|
+
| **`includeRawId`** | <code>boolean</code> | Return the raw Widevine device unique ID as base64. Defaults to `false`. |
|
|
282
|
+
| **`hashSalt`** | <code>string</code> | Optional salt used to derive `fingerprint`. Android uses the app package name when omitted. |
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
#### PrepareAttestationResult
|
|
286
|
+
|
|
287
|
+
| Prop | Type | Description |
|
|
288
|
+
| ------------ | --------------------------------------------------------------- | ------------------------------------------------------------------- |
|
|
289
|
+
| **`keyId`** | <code>string</code> | iOS App Attest key ID or Android Play Integrity provider handle ID. |
|
|
290
|
+
| **`format`** | <code><a href="#attestationformat">AttestationFormat</a></code> | Native attestation format used by the current platform. |
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
#### PrepareAttestationOptions
|
|
294
|
+
|
|
295
|
+
| Prop | Type | Description |
|
|
296
|
+
| ------------------------ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
297
|
+
| **`cloudProjectNumber`** | <code>string</code> | Android only. Google Cloud project number for Play Integrity. Can be configured globally via `plugins.DeviceIntegrity.cloudProjectNumber`. |
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
#### AttestationTokenResult
|
|
301
|
+
|
|
302
|
+
| Prop | Type | Description |
|
|
303
|
+
| ------------ | --------------------------------------------------------------- | ------------------------------------------------------- |
|
|
304
|
+
| **`token`** | <code>string</code> | Token/assertion that must be verified server-side. |
|
|
305
|
+
| **`keyId`** | <code>string</code> | Key/provider handle used to create the token. |
|
|
306
|
+
| **`format`** | <code><a href="#attestationformat">AttestationFormat</a></code> | Native attestation format used by the current platform. |
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
#### CreateAttestationOptions
|
|
310
|
+
|
|
311
|
+
| Prop | Type | Description |
|
|
312
|
+
| ------------------------ | ------------------- | ------------------------------------------------------------- |
|
|
313
|
+
| **`keyId`** | <code>string</code> | Key/provider handle returned from `prepareAttestation()`. |
|
|
314
|
+
| **`challenge`** | <code>string</code> | Backend-issued one-time challenge. |
|
|
315
|
+
| **`cloudProjectNumber`** | <code>string</code> | Android only. Google Cloud project number for Play Integrity. |
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
#### CreateAssertionOptions
|
|
319
|
+
|
|
320
|
+
| Prop | Type | Description |
|
|
321
|
+
| ------------------------ | ------------------- | ------------------------------------------------------------- |
|
|
322
|
+
| **`keyId`** | <code>string</code> | Key/provider handle returned from `prepareAttestation()`. |
|
|
323
|
+
| **`payload`** | <code>string</code> | Backend-issued one-time request payload or nonce. |
|
|
324
|
+
| **`cloudProjectNumber`** | <code>string</code> | Android only. Google Cloud project number for Play Integrity. |
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
#### DeviceCheckTokenResult
|
|
328
|
+
|
|
329
|
+
| Prop | Type | Description |
|
|
330
|
+
| ----------- | ------------------- | ---------------------------------------- |
|
|
331
|
+
| **`token`** | <code>string</code> | iOS DeviceCheck token encoded as base64. |
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
### Type Aliases
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
#### DeviceIntegrityPlatform
|
|
338
|
+
|
|
339
|
+
<code>'android' | 'ios' | 'web'</code>
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
#### AttestationFormat
|
|
343
|
+
|
|
344
|
+
<code>'apple-app-attest' | 'google-play-integrity-standard'</code>
|
|
345
|
+
|
|
346
|
+
</docgen-api>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
ext {
|
|
2
|
+
junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
|
|
3
|
+
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1'
|
|
4
|
+
androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.3.0'
|
|
5
|
+
androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0'
|
|
6
|
+
playIntegrityVersion = project.hasProperty('playIntegrityVersion') ? rootProject.ext.playIntegrityVersion : '1.6.0'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
buildscript {
|
|
10
|
+
repositories {
|
|
11
|
+
google()
|
|
12
|
+
mavenCentral()
|
|
13
|
+
}
|
|
14
|
+
dependencies {
|
|
15
|
+
classpath 'com.android.tools.build:gradle:8.13.0'
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
apply plugin: 'com.android.library'
|
|
20
|
+
|
|
21
|
+
android {
|
|
22
|
+
namespace = "app.capgo.deviceintegrity"
|
|
23
|
+
compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 36
|
|
24
|
+
defaultConfig {
|
|
25
|
+
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 24
|
|
26
|
+
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 36
|
|
27
|
+
versionCode 1
|
|
28
|
+
versionName "1.0"
|
|
29
|
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
30
|
+
}
|
|
31
|
+
buildTypes {
|
|
32
|
+
release {
|
|
33
|
+
minifyEnabled false
|
|
34
|
+
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
lint {
|
|
38
|
+
abortOnError = false
|
|
39
|
+
}
|
|
40
|
+
compileOptions {
|
|
41
|
+
sourceCompatibility JavaVersion.VERSION_21
|
|
42
|
+
targetCompatibility JavaVersion.VERSION_21
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
repositories {
|
|
47
|
+
google()
|
|
48
|
+
mavenCentral()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
dependencies {
|
|
53
|
+
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
54
|
+
implementation project(':capacitor-android')
|
|
55
|
+
annotationProcessor project(':capacitor-android')
|
|
56
|
+
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
57
|
+
implementation "com.google.android.play:integrity:$playIntegrityVersion"
|
|
58
|
+
testImplementation "junit:junit:$junitVersion"
|
|
59
|
+
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
60
|
+
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
61
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
package app.capgo.deviceintegrity;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.content.pm.PackageManager;
|
|
5
|
+
import android.media.MediaDrm;
|
|
6
|
+
import android.media.UnsupportedSchemeException;
|
|
7
|
+
import java.nio.charset.StandardCharsets;
|
|
8
|
+
import java.security.MessageDigest;
|
|
9
|
+
import java.security.NoSuchAlgorithmException;
|
|
10
|
+
import java.util.UUID;
|
|
11
|
+
|
|
12
|
+
public class DeviceIntegrity {
|
|
13
|
+
|
|
14
|
+
private static final UUID WIDEVINE_UUID = UUID.fromString("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed");
|
|
15
|
+
private static final String PROPERTY_SECURITY_LEVEL = "securityLevel";
|
|
16
|
+
private static final String STANDARD_BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
17
|
+
private static final String URL_SAFE_BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
18
|
+
|
|
19
|
+
private final WidevineDrmReader widevineDrmReader;
|
|
20
|
+
|
|
21
|
+
public DeviceIntegrity() {
|
|
22
|
+
this(new AndroidWidevineDrmReader());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
DeviceIntegrity(WidevineDrmReader widevineDrmReader) {
|
|
26
|
+
this.widevineDrmReader = widevineDrmReader;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public WidevineCapabilities getWidevineCapabilities() {
|
|
30
|
+
if (!widevineDrmReader.isWidevineSupported()) {
|
|
31
|
+
return new WidevineCapabilities(false, false, false);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
WidevineData data = widevineDrmReader.readWidevineData();
|
|
36
|
+
return new WidevineCapabilities(
|
|
37
|
+
true,
|
|
38
|
+
data.deviceUniqueId != null && data.deviceUniqueId.length > 0,
|
|
39
|
+
!isBlank(data.securityLevel)
|
|
40
|
+
);
|
|
41
|
+
} catch (DeviceIntegrityException error) {
|
|
42
|
+
return new WidevineCapabilities(true, false, false);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public WidevineFingerprint getWidevineFingerprint(String packageName, boolean includeRawId, String hashSalt)
|
|
47
|
+
throws DeviceIntegrityException {
|
|
48
|
+
if (!widevineDrmReader.isWidevineSupported()) {
|
|
49
|
+
throw new DeviceIntegrityException("Widevine DRM is not supported on this device");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
WidevineData data = widevineDrmReader.readWidevineData();
|
|
53
|
+
if (data.deviceUniqueId == null || data.deviceUniqueId.length == 0) {
|
|
54
|
+
throw new DeviceIntegrityException("Widevine device unique ID is not available on this device");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
String salt = isBlank(hashSalt) ? packageName : hashSalt;
|
|
58
|
+
byte[] fingerprintInput = joinSaltAndId(salt, data.deviceUniqueId);
|
|
59
|
+
|
|
60
|
+
return new WidevineFingerprint(
|
|
61
|
+
sha256Hex(fingerprintInput),
|
|
62
|
+
sha256Hex(data.deviceUniqueId),
|
|
63
|
+
includeRawId ? base64Encode(data.deviceUniqueId, false, true) : null,
|
|
64
|
+
data.securityLevel,
|
|
65
|
+
data.vendor,
|
|
66
|
+
data.version,
|
|
67
|
+
data.description
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public boolean isPlayIntegritySupported(Context context) {
|
|
72
|
+
try {
|
|
73
|
+
context.getPackageManager().getPackageInfo("com.android.vending", 0);
|
|
74
|
+
return true;
|
|
75
|
+
} catch (PackageManager.NameNotFoundException error) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public String createRequestHash(String payload) throws NoSuchAlgorithmException {
|
|
81
|
+
byte[] hash = sha256(payload.getBytes(StandardCharsets.UTF_8));
|
|
82
|
+
return base64Encode(hash, true, false);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static String sha256Hex(byte[] value) throws DeviceIntegrityException {
|
|
86
|
+
try {
|
|
87
|
+
byte[] hash = sha256(value);
|
|
88
|
+
StringBuilder result = new StringBuilder(hash.length * 2);
|
|
89
|
+
for (byte b : hash) {
|
|
90
|
+
result.append(String.format("%02x", b & 0xff));
|
|
91
|
+
}
|
|
92
|
+
return result.toString();
|
|
93
|
+
} catch (NoSuchAlgorithmException error) {
|
|
94
|
+
throw new DeviceIntegrityException("SHA-256 is not available", error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private static byte[] sha256(byte[] value) throws NoSuchAlgorithmException {
|
|
99
|
+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
100
|
+
return digest.digest(value);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private static byte[] joinSaltAndId(String salt, byte[] deviceUniqueId) {
|
|
104
|
+
byte[] saltBytes = salt.getBytes(StandardCharsets.UTF_8);
|
|
105
|
+
byte[] result = new byte[saltBytes.length + 1 + deviceUniqueId.length];
|
|
106
|
+
System.arraycopy(saltBytes, 0, result, 0, saltBytes.length);
|
|
107
|
+
result[saltBytes.length] = 0;
|
|
108
|
+
System.arraycopy(deviceUniqueId, 0, result, saltBytes.length + 1, deviceUniqueId.length);
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
static String base64Encode(byte[] input, boolean urlSafe, boolean padding) {
|
|
113
|
+
if (input.length == 0) {
|
|
114
|
+
return "";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
String alphabet = urlSafe ? URL_SAFE_BASE64 : STANDARD_BASE64;
|
|
118
|
+
StringBuilder output = new StringBuilder(((input.length + 2) / 3) * 4);
|
|
119
|
+
|
|
120
|
+
for (int index = 0; index < input.length; index += 3) {
|
|
121
|
+
int remaining = input.length - index;
|
|
122
|
+
int first = input[index] & 0xff;
|
|
123
|
+
int second = remaining > 1 ? input[index + 1] & 0xff : 0;
|
|
124
|
+
int third = remaining > 2 ? input[index + 2] & 0xff : 0;
|
|
125
|
+
|
|
126
|
+
int triple = (first << 16) | (second << 8) | third;
|
|
127
|
+
output.append(alphabet.charAt((triple >> 18) & 0x3f));
|
|
128
|
+
output.append(alphabet.charAt((triple >> 12) & 0x3f));
|
|
129
|
+
|
|
130
|
+
if (remaining > 1) {
|
|
131
|
+
output.append(alphabet.charAt((triple >> 6) & 0x3f));
|
|
132
|
+
} else if (padding) {
|
|
133
|
+
output.append('=');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (remaining > 2) {
|
|
137
|
+
output.append(alphabet.charAt(triple & 0x3f));
|
|
138
|
+
} else if (padding) {
|
|
139
|
+
output.append('=');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return output.toString();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private static boolean isBlank(String value) {
|
|
147
|
+
return value == null || value.trim().isEmpty();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface WidevineDrmReader {
|
|
151
|
+
boolean isWidevineSupported();
|
|
152
|
+
|
|
153
|
+
WidevineData readWidevineData() throws DeviceIntegrityException;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static final class AndroidWidevineDrmReader implements WidevineDrmReader {
|
|
157
|
+
|
|
158
|
+
@Override
|
|
159
|
+
public boolean isWidevineSupported() {
|
|
160
|
+
try {
|
|
161
|
+
return MediaDrm.isCryptoSchemeSupported(WIDEVINE_UUID);
|
|
162
|
+
} catch (RuntimeException error) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@Override
|
|
168
|
+
public WidevineData readWidevineData() throws DeviceIntegrityException {
|
|
169
|
+
MediaDrm mediaDrm = null;
|
|
170
|
+
try {
|
|
171
|
+
mediaDrm = new MediaDrm(WIDEVINE_UUID);
|
|
172
|
+
return new WidevineData(
|
|
173
|
+
readDeviceUniqueId(mediaDrm),
|
|
174
|
+
readStringProperty(mediaDrm, PROPERTY_SECURITY_LEVEL),
|
|
175
|
+
readStringProperty(mediaDrm, MediaDrm.PROPERTY_VENDOR),
|
|
176
|
+
readStringProperty(mediaDrm, MediaDrm.PROPERTY_VERSION),
|
|
177
|
+
readStringProperty(mediaDrm, MediaDrm.PROPERTY_DESCRIPTION)
|
|
178
|
+
);
|
|
179
|
+
} catch (UnsupportedSchemeException error) {
|
|
180
|
+
throw new DeviceIntegrityException("Widevine DRM is not supported on this device", error);
|
|
181
|
+
} catch (RuntimeException error) {
|
|
182
|
+
throw new DeviceIntegrityException("Unable to read Widevine DRM properties", error);
|
|
183
|
+
} finally {
|
|
184
|
+
if (mediaDrm != null) {
|
|
185
|
+
mediaDrm.release();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private byte[] readDeviceUniqueId(MediaDrm mediaDrm) {
|
|
191
|
+
try {
|
|
192
|
+
return mediaDrm.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID);
|
|
193
|
+
} catch (RuntimeException error) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private String readStringProperty(MediaDrm mediaDrm, String propertyName) {
|
|
199
|
+
try {
|
|
200
|
+
return mediaDrm.getPropertyString(propertyName);
|
|
201
|
+
} catch (RuntimeException error) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
static final class WidevineCapabilities {
|
|
208
|
+
|
|
209
|
+
final boolean supported;
|
|
210
|
+
final boolean fingerprintAvailable;
|
|
211
|
+
final boolean securityLevelScanSupported;
|
|
212
|
+
|
|
213
|
+
WidevineCapabilities(boolean supported, boolean fingerprintAvailable, boolean securityLevelScanSupported) {
|
|
214
|
+
this.supported = supported;
|
|
215
|
+
this.fingerprintAvailable = fingerprintAvailable;
|
|
216
|
+
this.securityLevelScanSupported = securityLevelScanSupported;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
static final class WidevineData {
|
|
221
|
+
|
|
222
|
+
final byte[] deviceUniqueId;
|
|
223
|
+
final String securityLevel;
|
|
224
|
+
final String vendor;
|
|
225
|
+
final String version;
|
|
226
|
+
final String description;
|
|
227
|
+
|
|
228
|
+
WidevineData(byte[] deviceUniqueId, String securityLevel, String vendor, String version, String description) {
|
|
229
|
+
this.deviceUniqueId = deviceUniqueId;
|
|
230
|
+
this.securityLevel = securityLevel;
|
|
231
|
+
this.vendor = vendor;
|
|
232
|
+
this.version = version;
|
|
233
|
+
this.description = description;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
static final class WidevineFingerprint {
|
|
238
|
+
|
|
239
|
+
final String fingerprint;
|
|
240
|
+
final String widevineIdSha256;
|
|
241
|
+
final String widevineIdBase64;
|
|
242
|
+
final String securityLevel;
|
|
243
|
+
final String vendor;
|
|
244
|
+
final String version;
|
|
245
|
+
final String description;
|
|
246
|
+
|
|
247
|
+
WidevineFingerprint(
|
|
248
|
+
String fingerprint,
|
|
249
|
+
String widevineIdSha256,
|
|
250
|
+
String widevineIdBase64,
|
|
251
|
+
String securityLevel,
|
|
252
|
+
String vendor,
|
|
253
|
+
String version,
|
|
254
|
+
String description
|
|
255
|
+
) {
|
|
256
|
+
this.fingerprint = fingerprint;
|
|
257
|
+
this.widevineIdSha256 = widevineIdSha256;
|
|
258
|
+
this.widevineIdBase64 = widevineIdBase64;
|
|
259
|
+
this.securityLevel = securityLevel;
|
|
260
|
+
this.vendor = vendor;
|
|
261
|
+
this.version = version;
|
|
262
|
+
this.description = description;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
static final class DeviceIntegrityException extends Exception {
|
|
267
|
+
|
|
268
|
+
DeviceIntegrityException(String message) {
|
|
269
|
+
super(message);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
DeviceIntegrityException(String message, Throwable cause) {
|
|
273
|
+
super(message, cause);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|