@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/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&lt;<a href="#deviceintegritycapabilities">DeviceIntegrityCapabilities</a>&gt;</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&lt;<a href="#widevinefingerprintresult">WidevineFingerprintResult</a>&gt;</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&lt;<a href="#prepareattestationresult">PrepareAttestationResult</a>&gt;</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&lt;<a href="#attestationtokenresult">AttestationTokenResult</a>&gt;</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&lt;<a href="#attestationtokenresult">AttestationTokenResult</a>&gt;</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&lt;<a href="#devicechecktokenresult">DeviceCheckTokenResult</a>&gt;</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,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -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
+ }