react-native-app-attestation 0.1.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/README.md +451 -0
- package/android/build.gradle +31 -0
- package/android/src/main/java/com/reactnativeappattestation/PlayIntegrityModule.kt +38 -0
- package/android/src/main/java/com/reactnativeappattestation/PlayIntegrityPackage.kt +21 -0
- package/ios/AppAttestModule.m +11 -0
- package/ios/AppAttestModule.swift +56 -0
- package/ios/ReactNativeAppAttestation-Bridging-Header.h +1 -0
- package/lib/AttestationService.d.ts +15 -0
- package/lib/AttestationService.js +126 -0
- package/lib/DeviceIDService.d.ts +9 -0
- package/lib/DeviceIDService.js +62 -0
- package/lib/index.d.ts +64 -0
- package/lib/index.js +171 -0
- package/lib/interceptors.d.ts +18 -0
- package/lib/interceptors.js +56 -0
- package/lib/types.d.ts +38 -0
- package/lib/types.js +3 -0
- package/package.json +49 -0
- package/react-native-app-attestation.podspec +27 -0
- package/src/AttestationService.ts +123 -0
- package/src/DeviceIDService.ts +62 -0
- package/src/index.ts +174 -0
- package/src/interceptors.ts +72 -0
- package/src/types.ts +52 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// src/AttestationService.ts
|
|
2
|
+
|
|
3
|
+
import { Platform, NativeModules } from 'react-native';
|
|
4
|
+
import { AttestationConfig, AttestationResult } from './types';
|
|
5
|
+
|
|
6
|
+
export class AttestationService {
|
|
7
|
+
private config: AttestationConfig;
|
|
8
|
+
private cachedToken: string | null = null;
|
|
9
|
+
private tokenExpiry: number | null = null;
|
|
10
|
+
private cacheDuration: number;
|
|
11
|
+
|
|
12
|
+
constructor(config: AttestationConfig) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.cacheDuration = config.tokenCacheDurationMs ?? 10 * 60 * 1000;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private log(msg: string) {
|
|
18
|
+
if (this.config.debug) console.log(`[Attestation] ${msg}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Backend se nonce fetch karo
|
|
22
|
+
private async fetchNonce(): Promise<string> {
|
|
23
|
+
const response = await fetch(this.config.nonceEndpoint);
|
|
24
|
+
const data = await response.json();
|
|
25
|
+
if (!data.nonce) throw new Error('Nonce nahi mila backend se');
|
|
26
|
+
return data.nonce;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Android — Play Integrity
|
|
30
|
+
private async getAndroidToken(): Promise<string | null> {
|
|
31
|
+
try {
|
|
32
|
+
const { PlayIntegrityModule } = NativeModules;
|
|
33
|
+
|
|
34
|
+
if (!PlayIntegrityModule) {
|
|
35
|
+
this.log('PlayIntegrityModule nahi mila');
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const nonce = await this.fetchNonce();
|
|
40
|
+
const token = await PlayIntegrityModule
|
|
41
|
+
.getAttestationToken(nonce);
|
|
42
|
+
this.log('Android token mila!');
|
|
43
|
+
return token;
|
|
44
|
+
|
|
45
|
+
} catch (error) {
|
|
46
|
+
this.log(`Android error: ${error}`);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// iOS — App Attest
|
|
52
|
+
private async getIOSToken(): Promise<string | null> {
|
|
53
|
+
try {
|
|
54
|
+
const { AppAttestModule } = NativeModules;
|
|
55
|
+
|
|
56
|
+
if (!AppAttestModule) {
|
|
57
|
+
this.log('AppAttestModule nahi mila');
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const challenge = await this.fetchNonce();
|
|
62
|
+
const token = await AppAttestModule
|
|
63
|
+
.getAttestationToken(challenge);
|
|
64
|
+
this.log('iOS token mila!');
|
|
65
|
+
return token;
|
|
66
|
+
|
|
67
|
+
} catch (error) {
|
|
68
|
+
this.log(`iOS error: ${error}`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Main function
|
|
74
|
+
async getToken(forceRefresh = false): Promise<AttestationResult> {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const platform = Platform.OS as 'android' | 'ios';
|
|
77
|
+
|
|
78
|
+
// Cache valid hai?
|
|
79
|
+
if (
|
|
80
|
+
!forceRefresh &&
|
|
81
|
+
this.cachedToken &&
|
|
82
|
+
this.tokenExpiry &&
|
|
83
|
+
now < this.tokenExpiry
|
|
84
|
+
) {
|
|
85
|
+
this.log('Cached token use ho raha hai');
|
|
86
|
+
return {
|
|
87
|
+
token: this.cachedToken,
|
|
88
|
+
fromCache: true,
|
|
89
|
+
platform,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Fresh token lo
|
|
94
|
+
let token: string | null = null;
|
|
95
|
+
|
|
96
|
+
if (Platform.OS === 'android') {
|
|
97
|
+
token = await this.getAndroidToken();
|
|
98
|
+
} else if (Platform.OS === 'ios') {
|
|
99
|
+
token = await this.getIOSToken();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Cache karo
|
|
103
|
+
if (token) {
|
|
104
|
+
this.cachedToken = token;
|
|
105
|
+
this.tokenExpiry = now + this.cacheDuration;
|
|
106
|
+
this.log(`Token cached — ${this.cacheDuration / 60000} min valid`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { token, fromCache: false, platform };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Sensitive operations ke liye
|
|
113
|
+
async getFreshToken(): Promise<AttestationResult> {
|
|
114
|
+
return this.getToken(true);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Logout pe call karo
|
|
118
|
+
clearCache(): void {
|
|
119
|
+
this.cachedToken = null;
|
|
120
|
+
this.tokenExpiry = null;
|
|
121
|
+
this.log('Cache cleared');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/DeviceIDService.ts
|
|
2
|
+
|
|
3
|
+
import { StorageAdapter } from './types';
|
|
4
|
+
|
|
5
|
+
const DEVICE_ID_KEY = 'rn_attestation_device_id';
|
|
6
|
+
|
|
7
|
+
// Khud ka UUID generator — koi library nahi!
|
|
8
|
+
const generateUUID = (): string => {
|
|
9
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
10
|
+
const r = (Math.random() * 16) | 0;
|
|
11
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
12
|
+
return v.toString(16);
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class DeviceIDService {
|
|
17
|
+
private storage: StorageAdapter;
|
|
18
|
+
private debug: boolean;
|
|
19
|
+
|
|
20
|
+
constructor(storage: StorageAdapter, debug = false) {
|
|
21
|
+
this.storage = storage;
|
|
22
|
+
this.debug = debug;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private log(msg: string) {
|
|
26
|
+
if (this.debug) console.log(`[DeviceID] ${msg}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async getDeviceID(): Promise<string> {
|
|
30
|
+
try {
|
|
31
|
+
// Storage se check karo
|
|
32
|
+
let deviceId = await Promise.resolve(
|
|
33
|
+
this.storage.get(DEVICE_ID_KEY)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (!deviceId) {
|
|
37
|
+
// Pehli baar — naya UUID banao
|
|
38
|
+
deviceId = generateUUID();
|
|
39
|
+
await Promise.resolve(
|
|
40
|
+
this.storage.set(DEVICE_ID_KEY, deviceId)
|
|
41
|
+
);
|
|
42
|
+
this.log(`Naya Device ID banaya: ${deviceId}`);
|
|
43
|
+
} else {
|
|
44
|
+
this.log(`Existing Device ID mila: ${deviceId}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return deviceId;
|
|
48
|
+
|
|
49
|
+
} catch (error) {
|
|
50
|
+
this.log(`Error: ${error}`);
|
|
51
|
+
// Fallback — storage fail ho toh bhi app crash na ho
|
|
52
|
+
return generateUUID();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async resetDeviceID(): Promise<void> {
|
|
57
|
+
await Promise.resolve(
|
|
58
|
+
this.storage.delete(DEVICE_ID_KEY)
|
|
59
|
+
);
|
|
60
|
+
this.log('Device ID reset ho gaya');
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
|
|
3
|
+
import { Platform } from 'react-native';
|
|
4
|
+
import { DeviceIDService } from './DeviceIDService';
|
|
5
|
+
import { AttestationService } from './AttestationService';
|
|
6
|
+
import { axiosInterceptor, secureFetch } from './interceptors';
|
|
7
|
+
import {
|
|
8
|
+
AttestationConfig,
|
|
9
|
+
SecurityHeaders,
|
|
10
|
+
StorageAdapter,
|
|
11
|
+
} from './types';
|
|
12
|
+
|
|
13
|
+
// Re-export — user directly import kar sake
|
|
14
|
+
export * from './types';
|
|
15
|
+
export { DeviceIDService } from './DeviceIDService';
|
|
16
|
+
export { AttestationService } from './AttestationService';
|
|
17
|
+
export { axiosInterceptor, secureFetch } from './interceptors';
|
|
18
|
+
|
|
19
|
+
// ========================================
|
|
20
|
+
// SINGLETON — ek baar init, har jagah use
|
|
21
|
+
// ========================================
|
|
22
|
+
let deviceIDService: DeviceIDService | null = null;
|
|
23
|
+
let attestationService: AttestationService | null = null;
|
|
24
|
+
let globalConfig: AttestationConfig | null = null;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* App start pe ek baar call karo
|
|
28
|
+
*
|
|
29
|
+
* MMKV example:
|
|
30
|
+
* initAttestation({
|
|
31
|
+
* storage: {
|
|
32
|
+
* get: (key) => mmkv.getString(key) ?? null,
|
|
33
|
+
* set: (key, val) => mmkv.set(key, val),
|
|
34
|
+
* delete: (key) => mmkv.delete(key),
|
|
35
|
+
* },
|
|
36
|
+
* nonceEndpoint: 'https://api.yourapp.com/auth/nonce',
|
|
37
|
+
* appVersion: '1.4',
|
|
38
|
+
* debug: __DEV__,
|
|
39
|
+
* });
|
|
40
|
+
*/
|
|
41
|
+
export const initAttestation = (config: AttestationConfig): void => {
|
|
42
|
+
globalConfig = config;
|
|
43
|
+
deviceIDService = new DeviceIDService(config.storage, config.debug);
|
|
44
|
+
attestationService = new AttestationService(config);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Init check
|
|
48
|
+
const checkInit = (fnName: string) => {
|
|
49
|
+
if (!deviceIDService || !attestationService || !globalConfig) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`[react-native-app-attestation] ${fnName}() call karne se pehle initAttestation() call karo!`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Device ID lo
|
|
58
|
+
*/
|
|
59
|
+
export const getDeviceID = async (): Promise<string> => {
|
|
60
|
+
checkInit('getDeviceID');
|
|
61
|
+
return deviceIDService!.getDeviceID();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Attestation token lo (cached ya fresh)
|
|
66
|
+
*/
|
|
67
|
+
export const getAttestationToken = async (
|
|
68
|
+
forceRefresh = false
|
|
69
|
+
): Promise<string | null> => {
|
|
70
|
+
checkInit('getAttestationToken');
|
|
71
|
+
const result = await attestationService!.getToken(forceRefresh);
|
|
72
|
+
return result.token;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Sensitive operations ke liye fresh token
|
|
77
|
+
* Payment, Login, OTP pe ye use karo
|
|
78
|
+
*/
|
|
79
|
+
export const getFreshAttestationToken = async (): Promise<string | null> => {
|
|
80
|
+
checkInit('getFreshAttestationToken');
|
|
81
|
+
const result = await attestationService!.getFreshToken();
|
|
82
|
+
return result.token;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Saare security headers ek saath lo
|
|
87
|
+
* Interceptor mein ye use karo
|
|
88
|
+
*/
|
|
89
|
+
export const getSecurityHeaders = async (): Promise<SecurityHeaders> => {
|
|
90
|
+
checkInit('getSecurityHeaders');
|
|
91
|
+
|
|
92
|
+
const [deviceId, attestationResult] = await Promise.all([
|
|
93
|
+
deviceIDService!.getDeviceID(),
|
|
94
|
+
attestationService!.getToken(),
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
98
|
+
const platform = Platform.OS === 'ios' ? 'iOS' : 'Android';
|
|
99
|
+
|
|
100
|
+
const headers: SecurityHeaders = {
|
|
101
|
+
'User-Agent': `App/${globalConfig!.appVersion} (${platform})`,
|
|
102
|
+
'X-App-Platform': Platform.OS,
|
|
103
|
+
'X-App-Version': globalConfig!.appVersion,
|
|
104
|
+
'X-Device-ID': deviceId,
|
|
105
|
+
'X-Timestamp': timestamp,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (attestationResult.token) {
|
|
109
|
+
headers['X-Attestation'] = attestationResult.token;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return headers;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Axios interceptor setup
|
|
117
|
+
*
|
|
118
|
+
* import axios from 'axios';
|
|
119
|
+
* import { initAttestation, setupAxios } from 'react-native-app-attestation';
|
|
120
|
+
*
|
|
121
|
+
* const api = axios.create({ baseURL: '...' });
|
|
122
|
+
* setupAxios(api);
|
|
123
|
+
*/
|
|
124
|
+
export const setupAxios = (axiosInstance: any): void => {
|
|
125
|
+
checkInit('setupAxios');
|
|
126
|
+
axiosInterceptor(axiosInstance, {
|
|
127
|
+
appVersion: globalConfig!.appVersion,
|
|
128
|
+
getHeaders: getSecurityHeaders,
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Secure fetch — fetch ki jagah use karo
|
|
134
|
+
*
|
|
135
|
+
* const res = await secureGet('https://api.com/user');
|
|
136
|
+
*/
|
|
137
|
+
export const secureGet = (
|
|
138
|
+
url: string,
|
|
139
|
+
options?: RequestInit
|
|
140
|
+
): Promise<Response> => {
|
|
141
|
+
checkInit('secureGet');
|
|
142
|
+
return secureFetch(url, options ?? {}, getSecurityHeaders);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const securePost = (
|
|
146
|
+
url: string,
|
|
147
|
+
body: unknown,
|
|
148
|
+
options?: RequestInit
|
|
149
|
+
): Promise<Response> => {
|
|
150
|
+
checkInit('securePost');
|
|
151
|
+
return secureFetch(
|
|
152
|
+
url,
|
|
153
|
+
{
|
|
154
|
+
...options,
|
|
155
|
+
method: 'POST',
|
|
156
|
+
body: JSON.stringify(body),
|
|
157
|
+
},
|
|
158
|
+
getSecurityHeaders
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Logout pe call karo
|
|
164
|
+
*/
|
|
165
|
+
export const clearAttestationCache = (): void => {
|
|
166
|
+
attestationService?.clearCache();
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Device ID reset karo
|
|
171
|
+
*/
|
|
172
|
+
export const resetDeviceID = async (): Promise<void> => {
|
|
173
|
+
await deviceIDService?.resetDeviceID();
|
|
174
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/interceptors.ts
|
|
2
|
+
|
|
3
|
+
import { SecurityHeaders } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AXIOS ke liye
|
|
7
|
+
*
|
|
8
|
+
* Use karo:
|
|
9
|
+
* axiosInterceptor(api, { appVersion: '1.4', getHeaders })
|
|
10
|
+
*/
|
|
11
|
+
export const axiosInterceptor = (
|
|
12
|
+
axiosInstance: any,
|
|
13
|
+
options: {
|
|
14
|
+
appVersion: string;
|
|
15
|
+
getHeaders: () => Promise<SecurityHeaders>;
|
|
16
|
+
}
|
|
17
|
+
) => {
|
|
18
|
+
axiosInstance.interceptors.request.use(
|
|
19
|
+
async (config: any) => {
|
|
20
|
+
try {
|
|
21
|
+
// Security headers lo
|
|
22
|
+
const securityHeaders = await options.getHeaders();
|
|
23
|
+
|
|
24
|
+
// Existing headers ke saath merge karo
|
|
25
|
+
config.headers = {
|
|
26
|
+
...config.headers,
|
|
27
|
+
...securityHeaders,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.warn('[Attestation] Headers add nahi ho sake:', error);
|
|
32
|
+
// App crash nahi hona chahiye — silently fail
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return config;
|
|
36
|
+
},
|
|
37
|
+
(error: any) => Promise.reject(error)
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* FETCH ke liye
|
|
43
|
+
*
|
|
44
|
+
* Use karo:
|
|
45
|
+
* const res = await secureFetch(url, options, getHeaders)
|
|
46
|
+
*/
|
|
47
|
+
export const secureFetch = async (
|
|
48
|
+
url: string,
|
|
49
|
+
options: RequestInit = {},
|
|
50
|
+
getHeaders: () => Promise<SecurityHeaders>
|
|
51
|
+
): Promise<Response> => {
|
|
52
|
+
try {
|
|
53
|
+
// Security headers lo
|
|
54
|
+
const securityHeaders = await getHeaders();
|
|
55
|
+
|
|
56
|
+
// Existing headers ke saath merge karo
|
|
57
|
+
const mergedHeaders = {
|
|
58
|
+
...options.headers,
|
|
59
|
+
...securityHeaders,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return fetch(url, {
|
|
63
|
+
...options,
|
|
64
|
+
headers: mergedHeaders,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.warn('[Attestation] secureFetch headers error:', error);
|
|
69
|
+
// Headers add nahi hue — normal fetch karo
|
|
70
|
+
return fetch(url, options);
|
|
71
|
+
}
|
|
72
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Storage interface — user apna storage dega
|
|
5
|
+
* MMKV, AsyncStorage, ya koi bhi
|
|
6
|
+
*/
|
|
7
|
+
export interface StorageAdapter {
|
|
8
|
+
get: (key: string) => string | null | Promise<string | null>;
|
|
9
|
+
set: (key: string, value: string) => void | Promise<void>;
|
|
10
|
+
delete: (key: string) => void | Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Package initialize karne ke liye config
|
|
15
|
+
*/
|
|
16
|
+
export interface AttestationConfig {
|
|
17
|
+
// Tumhara storage (MMKV ya AsyncStorage)
|
|
18
|
+
storage: StorageAdapter;
|
|
19
|
+
|
|
20
|
+
// Nonce fetch karne ke liye URL
|
|
21
|
+
nonceEndpoint: string;
|
|
22
|
+
|
|
23
|
+
// App version — header mein jayega
|
|
24
|
+
appVersion: string;
|
|
25
|
+
|
|
26
|
+
// Token cache kitni der valid rahe (default: 10 min)
|
|
27
|
+
tokenCacheDurationMs?: number;
|
|
28
|
+
|
|
29
|
+
// Debug logs on/off (default: false)
|
|
30
|
+
debug?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Attestation token result
|
|
35
|
+
*/
|
|
36
|
+
export interface AttestationResult {
|
|
37
|
+
token: string | null;
|
|
38
|
+
fromCache: boolean;
|
|
39
|
+
platform: 'android' | 'ios';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Security headers jo API call mein jayenge
|
|
44
|
+
*/
|
|
45
|
+
export interface SecurityHeaders {
|
|
46
|
+
'User-Agent': string;
|
|
47
|
+
'X-App-Platform': string;
|
|
48
|
+
'X-App-Version': string;
|
|
49
|
+
'X-Device-ID': string;
|
|
50
|
+
'X-Timestamp': string;
|
|
51
|
+
'X-Attestation'?: string;
|
|
52
|
+
}
|