@umutcansu/react-native-pinvault 2.4.2 → 2.5.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/CHANGELOG.md +17 -0
- package/README.md +15 -3
- package/RNPinVault.podspec +1 -1
- package/android/build.gradle +1 -1
- package/package.json +1 -1
- package/plugin/withPinVault.js +168 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.5.0 — 2026-10-12
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Expo: apps with their own Android backup rules build again, with
|
|
8
|
+
PinVault's excludes kept.** An app or plugin (`expo-secure-store`) that sets
|
|
9
|
+
`fullBackupContent` / `dataExtractionRules` failed the manifest merge
|
|
10
|
+
against the library's rules. At the end of prebuild the plugin now copies
|
|
11
|
+
those rules to `res/xml/pinvault_merged_<name>.xml` with PinVault's excludes
|
|
12
|
+
added (only where the app's includes reach) and points the manifest at the
|
|
13
|
+
copy with `tools:replace`; rules it cannot find stop prebuild. Built with
|
|
14
|
+
Expo SDK 54 + `expo-secure-store`, Android release.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Depends on PinVault 2.5.0 (Maven Central, Swift package tag `v2.5.0`).
|
|
19
|
+
|
|
3
20
|
## 2.4.2 — 2026-10-10
|
|
4
21
|
|
|
5
22
|
### Added
|
package/README.md
CHANGED
|
@@ -29,9 +29,9 @@ npm install @umutcansu/react-native-pinvault
|
|
|
29
29
|
cd ios && pod install
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
The native libraries come with it, at the package's own version: npm 2.
|
|
33
|
-
uses `io.github.umutcansu:pinvault:2.
|
|
34
|
-
package at tag `v2.
|
|
32
|
+
The native libraries come with it, at the package's own version: npm 2.5.0
|
|
33
|
+
uses `io.github.umutcansu:pinvault:2.5.0` from Maven Central and the Swift
|
|
34
|
+
package at tag `v2.5.0`. The New Architecture must stay on (React Native's
|
|
35
35
|
default since 0.76).
|
|
36
36
|
|
|
37
37
|
**iOS.** The pod depends on the PinVault Swift package through React Native's
|
|
@@ -102,6 +102,18 @@ npx expo install @umutcansu/react-native-pinvault
|
|
|
102
102
|
Unknown options and wrong types stop prebuild. Tested with Expo SDK 54
|
|
103
103
|
(React Native 0.81) on Android and iOS, release builds.
|
|
104
104
|
|
|
105
|
+
**Backup rules.** The Android library keeps its stores out of cloud backup
|
|
106
|
+
and device transfer with its own `fullBackupContent` / `dataExtractionRules`.
|
|
107
|
+
An app (or a plugin such as `expo-secure-store`) that sets its own rules used
|
|
108
|
+
to fail the manifest merge; replacing them by hand would drop PinVault's
|
|
109
|
+
excludes. At the end of prebuild the plugin copies each such rules
|
|
110
|
+
file (from the app or a library under `node_modules`) to
|
|
111
|
+
`res/xml/pinvault_merged_<name>.xml` with PinVault's excludes added, and
|
|
112
|
+
points the manifest at the copy with `tools:replace`. Rules it cannot find stop
|
|
113
|
+
prebuild. A bare React Native app with its own rules adds the
|
|
114
|
+
`<exclude>` lines of the library's `pinvault_backup_rules.xml` /
|
|
115
|
+
`pinvault_data_extraction_rules.xml` to them and uses `tools:replace`.
|
|
116
|
+
|
|
105
117
|
## Quick start
|
|
106
118
|
|
|
107
119
|
```ts
|
package/RNPinVault.podspec
CHANGED
|
@@ -4,7 +4,7 @@ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
|
4
4
|
|
|
5
5
|
# PinVault for iOS is a Swift package (Package.swift at the repository root,
|
|
6
6
|
# product "PinVault"). React Native's spm_dependency adds it to the Pods project:
|
|
7
|
-
# - consumers: the git URL at the tag of this package's version (v2.
|
|
7
|
+
# - consumers: the git URL at the tag of this package's version (v2.5.0);
|
|
8
8
|
# - development: PINVAULT_IOS_PACKAGE_PATH = a PinVault checkout (the sample's
|
|
9
9
|
# Podfile sets it to this repository), the counterpart of pinvault.localPath.
|
|
10
10
|
pinvault_url = "https://github.com/umutcansu/PinVault.git"
|
package/android/build.gradle
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umutcansu/react-native-pinvault",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "PinVault for React Native: dynamic certificate pinning with signed remote configs, mTLS enrollment and signed vault files, as a thin bridge over the native PinVault libraries",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
package/plugin/withPinVault.js
CHANGED
|
@@ -29,9 +29,11 @@ const {
|
|
|
29
29
|
withAppDelegate,
|
|
30
30
|
withDangerousMod,
|
|
31
31
|
withInfoPlist,
|
|
32
|
+
withFinalizedMod,
|
|
32
33
|
withPodfileProperties,
|
|
33
34
|
withXcodeProject,
|
|
34
35
|
createRunOncePlugin,
|
|
36
|
+
XML,
|
|
35
37
|
} = configPlugins;
|
|
36
38
|
|
|
37
39
|
const pkg = require('../package.json');
|
|
@@ -41,6 +43,29 @@ const BG_TASK_ID = 'io.github.umutcansu.pinvault.refresh';
|
|
|
41
43
|
const MIN_IOS = '16.0';
|
|
42
44
|
const ALLOW_NO_FILE_META = 'io.github.umutcansu.pinvault.ALLOW_NO_NATIVE_SECURITY_FILE';
|
|
43
45
|
const NETWORKING_PROVIDER = 'io.github.umutcansu.pinvault.reactnative.PinVaultNetworkingInitializer';
|
|
46
|
+
// PinVault's own backup rules (pinvault/src/main/res/xml): what must never
|
|
47
|
+
// reach a backup or a device transfer. The test checks this list against them.
|
|
48
|
+
const BACKUP_EXCLUDES = [
|
|
49
|
+
['sharedpref', 'pinvault_secure_config.xml'],
|
|
50
|
+
['sharedpref', 'pinvault_secure_client_cert.xml'],
|
|
51
|
+
['sharedpref', 'pinvault_secure_signing_keys.xml'],
|
|
52
|
+
['sharedpref', 'pinvault_secure_vault_files.xml'],
|
|
53
|
+
['sharedpref', 'pinvault_vault_file_versions.xml'],
|
|
54
|
+
['file', 'vault_files'],
|
|
55
|
+
['sharedpref', 'pinvault_client_cert.xml'],
|
|
56
|
+
['sharedpref', 'ssl_cert_config.xml'],
|
|
57
|
+
['sharedpref', 'ssl_cert_config_default.xml'],
|
|
58
|
+
['sharedpref', 'ssl_cert_config_default-tls.xml'],
|
|
59
|
+
['sharedpref', 'ssl_cert_config_secure-mtls.xml'],
|
|
60
|
+
['sharedpref', 'pinvault_signing_keys.xml'],
|
|
61
|
+
['sharedpref', 'pinvault_vault_files.xml'],
|
|
62
|
+
];
|
|
63
|
+
const OWN_RULES = {
|
|
64
|
+
'android:fullBackupContent': '@xml/pinvault_backup_rules',
|
|
65
|
+
'android:dataExtractionRules': '@xml/pinvault_data_extraction_rules',
|
|
66
|
+
};
|
|
67
|
+
const MERGED_PREFIX = 'pinvault_merged_';
|
|
68
|
+
const MERGED_FROM = /<!-- pinvault-merged-from: @xml\/([A-Za-z0-9_]+) -->/;
|
|
44
69
|
const KNOWN_OPTIONS = [
|
|
45
70
|
'nativeSecurityFile',
|
|
46
71
|
'faceIDPermission',
|
|
@@ -159,6 +184,136 @@ function applyAndroidManifest(manifest, props) {
|
|
|
159
184
|
return manifest;
|
|
160
185
|
}
|
|
161
186
|
|
|
187
|
+
/** Whether an `<include>` list (empty = everything) takes in [domain]/[file]. */
|
|
188
|
+
function included(includes, domain, file) {
|
|
189
|
+
if (includes.length === 0) return true;
|
|
190
|
+
return includes.some((i) => {
|
|
191
|
+
const d = i.$ && i.$.domain;
|
|
192
|
+
const p = String((i.$ && i.$.path) || '.').replace(/^\.\/?/, '').replace(/\/$/, '');
|
|
193
|
+
return d === domain && (p === '' || file === p || file.startsWith(`${p}/`));
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Adds PinVault's excludes to one rules element (a parsed `<full-backup-content>`
|
|
199
|
+
* or a section). Only where the element's includes reach: lint refuses an
|
|
200
|
+
* exclude outside every include (FullBackupContent), and such a file is not
|
|
201
|
+
* backed up anyway.
|
|
202
|
+
*/
|
|
203
|
+
function addExcludes(node) {
|
|
204
|
+
const out = node && typeof node === 'object' ? node : {};
|
|
205
|
+
const includes = Array.isArray(out.include) ? out.include : [];
|
|
206
|
+
out.exclude = Array.isArray(out.exclude) ? out.exclude : [];
|
|
207
|
+
for (const [domain, file] of BACKUP_EXCLUDES) {
|
|
208
|
+
if (!included(includes, domain, file)) continue;
|
|
209
|
+
if (!out.exclude.some((e) => e.$ && e.$.domain === domain && e.$.path === file)) {
|
|
210
|
+
out.exclude.push({ $: { domain, path: file } });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* An app's backup rules (parsed with XML.parseXMLAsync) with PinVault's
|
|
218
|
+
* excludes added. Excludes win over includes, so an app that includes
|
|
219
|
+
* `sharedpref/.` still keeps PinVault's stores out. A data-extraction section
|
|
220
|
+
* the app does not write (= everything) gets one with PinVault's excludes.
|
|
221
|
+
*/
|
|
222
|
+
function mergeBackupRules(xml) {
|
|
223
|
+
if (xml && 'full-backup-content' in xml) {
|
|
224
|
+
return { 'full-backup-content': addExcludes(xml['full-backup-content']) };
|
|
225
|
+
}
|
|
226
|
+
if (xml && 'data-extraction-rules' in xml) {
|
|
227
|
+
const root = xml['data-extraction-rules'] && typeof xml['data-extraction-rules'] === 'object' ? xml['data-extraction-rules'] : {};
|
|
228
|
+
for (const section of ['cloud-backup', 'device-transfer']) {
|
|
229
|
+
root[section] = (Array.isArray(root[section]) ? root[section] : [{}]).map(addExcludes);
|
|
230
|
+
}
|
|
231
|
+
if (Array.isArray(root['cross-platform-transfer'])) root['cross-platform-transfer'] = root['cross-platform-transfer'].map(addExcludes);
|
|
232
|
+
return { 'data-extraction-rules': root };
|
|
233
|
+
}
|
|
234
|
+
throw new Error(`[${pkg.name}] backup rules file has neither <full-backup-content> nor <data-extraction-rules>`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** `res/xml/<name>.xml` of the app, or of a library under node_modules (Expo modules ship theirs there). */
|
|
238
|
+
function findRulesFile(projectRoot, platformRoot, name) {
|
|
239
|
+
const file = path.join('android', 'src', 'main', 'res', 'xml', `${name}.xml`);
|
|
240
|
+
const candidates = [path.join(platformRoot, 'app', 'src', 'main', 'res', 'xml', `${name}.xml`)];
|
|
241
|
+
// node_modules of the project and its parents (monorepos hoist packages up).
|
|
242
|
+
for (let dir = path.resolve(projectRoot); ; dir = path.dirname(dir)) {
|
|
243
|
+
const modules = path.join(dir, 'node_modules');
|
|
244
|
+
if (fs.existsSync(modules)) {
|
|
245
|
+
for (const entry of fs.readdirSync(modules)) {
|
|
246
|
+
if (entry.startsWith('@')) {
|
|
247
|
+
for (const scoped of fs.readdirSync(path.join(modules, entry))) candidates.push(path.join(modules, entry, scoped, file));
|
|
248
|
+
} else {
|
|
249
|
+
candidates.push(path.join(modules, entry, file));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (path.dirname(dir) === dir) break;
|
|
254
|
+
}
|
|
255
|
+
return candidates.find((c) => fs.existsSync(c));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* AndroidManifest.xml on disk, after every other plugin: an app (or a plugin
|
|
260
|
+
* such as expo-secure-store) that sets its own `fullBackupContent` /
|
|
261
|
+
* `dataExtractionRules` clashed with PinVault's at the manifest merge, and a
|
|
262
|
+
* hand-written `tools:replace` would drop PinVault's excludes. Each such file
|
|
263
|
+
* is copied to `res/xml/pinvault_merged_<name>.xml` with PinVault's excludes
|
|
264
|
+
* added, and the manifest points at the copy with `tools:replace`. Idempotent:
|
|
265
|
+
* a rerun without --clean merges from the original again.
|
|
266
|
+
*/
|
|
267
|
+
async function mergeAndroidBackupRules(projectRoot, platformRoot) {
|
|
268
|
+
const manifestPath = path.join(platformRoot, 'app', 'src', 'main', 'AndroidManifest.xml');
|
|
269
|
+
const manifest = await XML.readXMLAsync({ path: manifestPath });
|
|
270
|
+
const app = AndroidConfig.Manifest.getMainApplicationOrThrow(manifest);
|
|
271
|
+
const resDir = path.join(platformRoot, 'app', 'src', 'main', 'res', 'xml');
|
|
272
|
+
const replace = new Set(String(app.$['tools:replace'] || '').split(',').map((s) => s.trim()).filter(Boolean));
|
|
273
|
+
let changed = false;
|
|
274
|
+
for (const attr of Object.keys(OWN_RULES)) {
|
|
275
|
+
const value = app.$[attr];
|
|
276
|
+
if (value === undefined || value === OWN_RULES[attr]) continue;
|
|
277
|
+
// fullBackupContent="false": no full backup at all, stricter than PinVault's rules.
|
|
278
|
+
if (attr === 'android:fullBackupContent' && value === 'false') {
|
|
279
|
+
if (!replace.has(attr)) { replace.add(attr); changed = true; }
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const ref = /^@xml\/([A-Za-z0-9_]+)$/.exec(value);
|
|
283
|
+
if (!ref) throw new Error(`[${pkg.name}] ${attr}="${value}" is not an @xml resource; merge PinVault's backup excludes by hand (README, "Expo")`);
|
|
284
|
+
let name = ref[1];
|
|
285
|
+
if (name.startsWith(MERGED_PREFIX)) {
|
|
286
|
+
const previous = path.join(resDir, `${name}.xml`);
|
|
287
|
+
const from = fs.existsSync(previous) ? MERGED_FROM.exec(fs.readFileSync(previous, 'utf8')) : null;
|
|
288
|
+
if (!from) throw new Error(`[${pkg.name}] ${previous} is missing or not ours; run expo prebuild --clean`);
|
|
289
|
+
name = from[1];
|
|
290
|
+
}
|
|
291
|
+
const source = findRulesFile(projectRoot, platformRoot, name);
|
|
292
|
+
if (!source) {
|
|
293
|
+
throw new Error(`[${pkg.name}] ${attr} points at @xml/${name}, which was not found in the app or node_modules; ` +
|
|
294
|
+
"PinVault's backup excludes cannot be merged into it (README, \"Expo\")");
|
|
295
|
+
}
|
|
296
|
+
const merged = mergeBackupRules(await XML.parseXMLAsync(fs.readFileSync(source, 'utf8')));
|
|
297
|
+
const out = `${MERGED_PREFIX}${name}`;
|
|
298
|
+
fs.mkdirSync(resDir, { recursive: true });
|
|
299
|
+
fs.writeFileSync(
|
|
300
|
+
path.join(resDir, `${out}.xml`),
|
|
301
|
+
'<?xml version="1.0" encoding="utf-8"?>\n' +
|
|
302
|
+
`<!-- pinvault-merged-from: @xml/${name} -->\n` +
|
|
303
|
+
`<!-- Written by ${pkg.name} at expo prebuild: these rules plus PinVault's excludes. -->\n` +
|
|
304
|
+
XML.format(merged) + '\n'
|
|
305
|
+
);
|
|
306
|
+
app.$[attr] = `@xml/${out}`;
|
|
307
|
+
replace.add(attr);
|
|
308
|
+
changed = true;
|
|
309
|
+
}
|
|
310
|
+
if (!changed) return false;
|
|
311
|
+
manifest.manifest.$['xmlns:tools'] = manifest.manifest.$['xmlns:tools'] || 'http://schemas.android.com/tools';
|
|
312
|
+
app.$['tools:replace'] = [...replace].join(',');
|
|
313
|
+
await XML.writeXMLAsync({ path: manifestPath, xml: manifest });
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
|
|
162
317
|
function withPinVault(config, options) {
|
|
163
318
|
const props = checkOptions(options);
|
|
164
319
|
|
|
@@ -189,6 +344,15 @@ function withPinVault(config, options) {
|
|
|
189
344
|
return c;
|
|
190
345
|
});
|
|
191
346
|
|
|
347
|
+
// Last of all: other plugins have written their backup rules by then.
|
|
348
|
+
config = withFinalizedMod(config, [
|
|
349
|
+
'android',
|
|
350
|
+
async (c) => {
|
|
351
|
+
await mergeAndroidBackupRules(c.modRequest.projectRoot, c.modRequest.platformProjectRoot);
|
|
352
|
+
return c;
|
|
353
|
+
},
|
|
354
|
+
]);
|
|
355
|
+
|
|
192
356
|
if (props.backgroundUpdates) {
|
|
193
357
|
config = withAppDelegate(config, (c) => {
|
|
194
358
|
c.modResults.contents = addBackgroundTaskToAppDelegate(c.modResults.contents, c.modResults.language);
|
|
@@ -229,4 +393,7 @@ function withPinVault(config, options) {
|
|
|
229
393
|
}
|
|
230
394
|
|
|
231
395
|
module.exports = createRunOncePlugin(withPinVault, pkg.name, pkg.version);
|
|
232
|
-
module.exports.internal = {
|
|
396
|
+
module.exports.internal = {
|
|
397
|
+
addBackgroundTaskToAppDelegate, applyInfoPlist, applyAndroidManifest, checkOptions, versionLess,
|
|
398
|
+
mergeBackupRules, mergeAndroidBackupRules, BACKUP_EXCLUDES,
|
|
399
|
+
};
|