@capgo/capacitor-ssl-pinning 8.1.5 → 8.1.7

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 CHANGED
@@ -54,7 +54,7 @@ const config: CapacitorConfig = {
54
54
  enabled: true,
55
55
  },
56
56
  SSLPinning: {
57
- certs: ['sslCerts/production/primary.cer', 'sslCerts/production/backup.cer'],
57
+ certs: ['sslCerts/production/primary.cer', 'sslCerts/production/backup.der'],
58
58
  excludedDomains: ['https://analytics.google.com'],
59
59
  },
60
60
  },
@@ -63,7 +63,7 @@ const config: CapacitorConfig = {
63
63
  export default config;
64
64
  ```
65
65
 
66
- During `bunx cap sync`, the plugin copies the configured certificates into `webDir/certs`, which lets both native implementations load them from the bundled app assets.
66
+ During `bunx cap sync`, the plugin copies the configured certificates into `webDir/certs`, which lets both native implementations load them from the bundled app assets. Both `.cer` and `.der` are supported; PEM `.cer` files are converted to DER automatically for iOS.
67
67
 
68
68
  ## Usage
69
69
 
@@ -3,6 +3,7 @@ package io.ionic.sslpinning;
3
3
  import android.content.Context;
4
4
  import com.getcapacitor.Bridge;
5
5
  import com.getcapacitor.PluginConfig;
6
+ import java.io.FileNotFoundException;
6
7
  import java.io.InputStream;
7
8
  import java.net.URL;
8
9
  import java.security.KeyStore;
@@ -91,9 +92,43 @@ final class CertificateLoader {
91
92
 
92
93
  private static X509Certificate loadCertificate(final Context context, final String configuredCertPath) throws Exception {
93
94
  final String fileName = configuredCertPath.substring(configuredCertPath.lastIndexOf('/') + 1);
95
+ final String[] candidateFileNames = candidateFileNames(fileName);
96
+ final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
97
+
98
+ for (String candidateFileName : candidateFileNames) {
99
+ try (InputStream inputStream = context.getAssets().open(CERTS_DIR + "/" + candidateFileName)) {
100
+ return (X509Certificate) certificateFactory.generateCertificate(inputStream);
101
+ } catch (FileNotFoundException ignored) {
102
+ // Try alternate extension if available.
103
+ }
104
+ }
105
+
106
+ // Trigger the original exception message for easier debugging.
94
107
  try (InputStream inputStream = context.getAssets().open(CERTS_DIR + "/" + fileName)) {
95
- final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
96
108
  return (X509Certificate) certificateFactory.generateCertificate(inputStream);
97
109
  }
98
110
  }
111
+
112
+ private static String[] candidateFileNames(final String fileName) {
113
+ if (fileName == null) {
114
+ return new String[0];
115
+ }
116
+
117
+ final int dotIndex = fileName.lastIndexOf('.');
118
+ if (dotIndex == -1) {
119
+ return new String[] { fileName };
120
+ }
121
+
122
+ final String base = fileName.substring(0, dotIndex);
123
+ final String ext = fileName.substring(dotIndex + 1).toLowerCase();
124
+
125
+ if ("cer".equals(ext)) {
126
+ return new String[] { fileName, base + ".der" };
127
+ }
128
+ if ("der".equals(ext)) {
129
+ return new String[] { fileName, base + ".cer" };
130
+ }
131
+
132
+ return new String[] { fileName };
133
+ }
99
134
  }
@@ -33,6 +33,10 @@ export interface SSLPinningPluginConfig {
33
33
  *
34
34
  * During `bunx cap sync`, this plugin copies them into `webDir/certs` so native runtimes can load them from
35
35
  * the bundled web assets.
36
+ *
37
+ * Both `.cer` and `.der` filenames are supported. iOS requires DER-encoded X.509 certificates; when a
38
+ * certificate file is provided in PEM form (for example a `.cer` containing `-----BEGIN CERTIFICATE-----`),
39
+ * the sync hook converts it to DER while keeping the same filename.
36
40
  */
37
41
  certs?: string[];
38
42
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Plugin version payload.\n */\nexport interface PluginVersionResult {\n /**\n * Version identifier returned by the platform implementation.\n */\n version: string;\n}\n\n/**\n * Static SSL pinning configuration currently visible to the plugin.\n */\nexport interface SSLPinningConfigurationState {\n /**\n * Whether at least one certificate is configured for native pinning.\n */\n configured: boolean;\n\n /**\n * Certificate paths from `capacitor.config.*` relative to the app root.\n */\n certs: string[];\n\n /**\n * Fully-qualified URLs that should bypass SSL pinning.\n */\n excludedDomains: string[];\n}\n\n/**\n * Plugin configuration added under `plugins.SSLPinning` in `capacitor.config.*`.\n */\nexport interface SSLPinningPluginConfig {\n /**\n * Certificate files relative to the application root.\n *\n * During `bunx cap sync`, this plugin copies them into `webDir/certs` so native runtimes can load them from\n * the bundled web assets.\n */\n certs?: string[];\n\n /**\n * Fully-qualified URLs that should bypass pinning.\n *\n * Match is done by origin, and by path-prefix when the configured URL contains a path.\n */\n excludedDomains?: string[];\n}\n\n/**\n * Capacitor API for inspecting SSL pinning configuration.\n *\n * Native enforcement is applied automatically to `CapacitorHttp` requests when:\n * 1. `plugins.CapacitorHttp.enabled` is `true`\n * 2. `plugins.SSLPinning.certs` contains at least one certificate\n */\nexport interface SSLPinningPlugin {\n /**\n * Returns the active native configuration visible to the plugin.\n */\n getConfiguration(): Promise<SSLPinningConfigurationState>;\n\n /**\n * Returns the native implementation version marker.\n */\n getPluginVersion(): Promise<PluginVersionResult>;\n}\n"]}
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Plugin version payload.\n */\nexport interface PluginVersionResult {\n /**\n * Version identifier returned by the platform implementation.\n */\n version: string;\n}\n\n/**\n * Static SSL pinning configuration currently visible to the plugin.\n */\nexport interface SSLPinningConfigurationState {\n /**\n * Whether at least one certificate is configured for native pinning.\n */\n configured: boolean;\n\n /**\n * Certificate paths from `capacitor.config.*` relative to the app root.\n */\n certs: string[];\n\n /**\n * Fully-qualified URLs that should bypass SSL pinning.\n */\n excludedDomains: string[];\n}\n\n/**\n * Plugin configuration added under `plugins.SSLPinning` in `capacitor.config.*`.\n */\nexport interface SSLPinningPluginConfig {\n /**\n * Certificate files relative to the application root.\n *\n * During `bunx cap sync`, this plugin copies them into `webDir/certs` so native runtimes can load them from\n * the bundled web assets.\n *\n * Both `.cer` and `.der` filenames are supported. iOS requires DER-encoded X.509 certificates; when a\n * certificate file is provided in PEM form (for example a `.cer` containing `-----BEGIN CERTIFICATE-----`),\n * the sync hook converts it to DER while keeping the same filename.\n */\n certs?: string[];\n\n /**\n * Fully-qualified URLs that should bypass pinning.\n *\n * Match is done by origin, and by path-prefix when the configured URL contains a path.\n */\n excludedDomains?: string[];\n}\n\n/**\n * Capacitor API for inspecting SSL pinning configuration.\n *\n * Native enforcement is applied automatically to `CapacitorHttp` requests when:\n * 1. `plugins.CapacitorHttp.enabled` is `true`\n * 2. `plugins.SSLPinning.certs` contains at least one certificate\n */\nexport interface SSLPinningPlugin {\n /**\n * Returns the active native configuration visible to the plugin.\n */\n getConfiguration(): Promise<SSLPinningConfigurationState>;\n\n /**\n * Returns the native implementation version marker.\n */\n getPluginVersion(): Promise<PluginVersionResult>;\n}\n"]}
@@ -85,8 +85,9 @@ final class SSLPinningCertificateStore {
85
85
 
86
86
  private func loadCertificate(from configuredPath: String) throws -> SecCertificate {
87
87
  let fileName = URL(fileURLWithPath: configuredPath).lastPathComponent
88
- guard let resourceURL = Bundle.main.resourceURL?.appendingPathComponent("public/certs/\(fileName)"),
89
- let certificateData = try? Data(contentsOf: resourceURL),
88
+ let candidateFileNames = SSLPinningCertificateStore.candidateFileNames(for: fileName)
89
+
90
+ guard let certificateData = SSLPinningCertificateStore.loadBundledCertificateData(candidateFileNames: candidateFileNames),
90
91
  let certificate = SecCertificateCreateWithData(nil, certificateData as CFData)
91
92
  else {
92
93
  throw NSError(
@@ -98,6 +99,73 @@ final class SSLPinningCertificateStore {
98
99
 
99
100
  return certificate
100
101
  }
102
+
103
+ private static func candidateFileNames(for fileName: String) -> [String] {
104
+ var candidates = [fileName]
105
+ let url = URL(fileURLWithPath: fileName)
106
+ let ext = url.pathExtension.lowercased()
107
+ let base = url.deletingPathExtension().lastPathComponent
108
+
109
+ if ext == "cer" {
110
+ candidates.append("\(base).der")
111
+ } else if ext == "der" {
112
+ candidates.append("\(base).cer")
113
+ }
114
+
115
+ return candidates
116
+ }
117
+
118
+ private static func loadBundledCertificateData(candidateFileNames: [String]) -> Data? {
119
+ guard let resourceURL = Bundle.main.resourceURL else {
120
+ return nil
121
+ }
122
+
123
+ for fileName in candidateFileNames {
124
+ let candidateURL = resourceURL.appendingPathComponent("public/certs/\(fileName)")
125
+ guard let data = try? Data(contentsOf: candidateURL) else {
126
+ continue
127
+ }
128
+
129
+ if let decoded = decodePEMCertificateIfNeeded(data) {
130
+ return decoded
131
+ }
132
+
133
+ return data
134
+ }
135
+
136
+ return nil
137
+ }
138
+
139
+ private static func decodePEMCertificateIfNeeded(_ data: Data) -> Data? {
140
+ guard let text = String(data: data, encoding: .utf8),
141
+ text.contains("-----BEGIN CERTIFICATE-----")
142
+ else {
143
+ return nil
144
+ }
145
+
146
+ let beginMarker = "-----BEGIN CERTIFICATE-----"
147
+ let endMarker = "-----END CERTIFICATE-----"
148
+
149
+ guard let beginRange = text.range(of: beginMarker),
150
+ let endRange = text.range(of: endMarker, range: beginRange.upperBound..<text.endIndex)
151
+ else {
152
+ return nil
153
+ }
154
+
155
+ let base64Body = text[beginRange.upperBound..<endRange.lowerBound]
156
+ .replacingOccurrences(of: "\r", with: "")
157
+ .replacingOccurrences(of: "\n", with: "")
158
+ .replacingOccurrences(of: " ", with: "")
159
+ .replacingOccurrences(of: "\t", with: "")
160
+
161
+ guard let decoded = Data(base64Encoded: String(base64Body), options: [.ignoreUnknownCharacters]),
162
+ !decoded.isEmpty
163
+ else {
164
+ return nil
165
+ }
166
+
167
+ return decoded
168
+ }
101
169
  }
102
170
 
103
171
  enum SSLPinningTrustEvaluator {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-ssl-pinning",
3
- "version": "8.1.5",
3
+ "version": "8.1.7",
4
4
  "description": "Capacitor SSL pinning plugin for Android and iOS that integrates with CapacitorHttp.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -55,8 +55,7 @@
55
55
  "capacitor:sync:before": "bun run prepare:certs",
56
56
  "capacitor:update:before": "bun run prepare:certs",
57
57
  "example:install": "cd example-app && bun install --frozen-lockfile",
58
- "example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build",
59
- "example:capgo:deploy": "bun run example:build && bun scripts/deploy-example-capgo.mjs"
58
+ "example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build"
60
59
  },
61
60
  "devDependencies": {
62
61
  "@capacitor/android": "^8.0.0",
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { cp, mkdir, readdir, rm } from 'node:fs/promises';
3
+ import { cp, mkdir, readdir, rm, readFile, writeFile } from 'node:fs/promises';
4
4
  import { basename, join, resolve } from 'node:path';
5
5
 
6
6
  const rootDir = process.env.CAPACITOR_ROOT_DIR;
@@ -12,6 +12,20 @@ const fail = (message) => {
12
12
  throw new Error(`[ssl-pinning] ${message}`);
13
13
  };
14
14
 
15
+ const pemToDer = (pem) => {
16
+ const match = pem.match(/-----BEGIN CERTIFICATE-----([\s\S]*?)-----END CERTIFICATE-----/);
17
+ if (!match) {
18
+ return null;
19
+ }
20
+
21
+ const base64 = match[1].replace(/[\r\n\s]/g, '');
22
+ if (!base64) {
23
+ return null;
24
+ }
25
+
26
+ return Buffer.from(base64, 'base64');
27
+ };
28
+
15
29
  if (!rootDir || !webDir || !configJson) {
16
30
  log('Capacitor hook environment not detected, skipping certificate preparation.');
17
31
  process.exit(0);
@@ -46,6 +60,19 @@ for (const existingEntry of await readdir(targetDir, { withFileTypes: true })) {
46
60
  }
47
61
 
48
62
  for (const { source, fileName } of sourceFiles) {
49
- await cp(source, join(targetDir, fileName), { force: true });
63
+ const targetPath = join(targetDir, fileName);
64
+ const buffer = await readFile(source);
65
+ const head = buffer.subarray(0, 2048).toString('utf8');
66
+ if (head.includes('-----BEGIN CERTIFICATE-----')) {
67
+ const der = pemToDer(buffer.toString('utf8'));
68
+ if (!der || der.length === 0) {
69
+ fail(`Failed to decode PEM certificate ${fileName}.`);
70
+ }
71
+ await writeFile(targetPath, der);
72
+ log(`Converted PEM ${fileName} into DER at ${targetDir}`);
73
+ continue;
74
+ }
75
+
76
+ await cp(source, targetPath, { force: true });
50
77
  log(`Copied ${fileName} into ${targetDir}`);
51
78
  }
@@ -1,117 +0,0 @@
1
- #!/usr/bin/env node
2
- import { existsSync, readFileSync } from 'node:fs';
3
- import { dirname, resolve } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { spawnSync } from 'node:child_process';
6
-
7
- const __dirname = dirname(fileURLToPath(import.meta.url));
8
- const repoRoot = resolve(__dirname, '..');
9
- const appDir = resolve(repoRoot, 'example-app');
10
- const rootPackageJson = JSON.parse(readFileSync(resolve(repoRoot, 'package.json'), 'utf8'));
11
- const examplePackageJson = JSON.parse(readFileSync(resolve(appDir, 'package.json'), 'utf8'));
12
- const tsConfigPath = resolve(appDir, 'capacitor.config.ts');
13
- const jsonConfigPath = resolve(appDir, 'capacitor.config.json');
14
- const configPath = existsSync(tsConfigPath) ? tsConfigPath : existsSync(jsonConfigPath) ? jsonConfigPath : null;
15
-
16
- if (!configPath) {
17
- console.error(
18
- 'Missing Capacitor config. Expected example-app/capacitor.config.ts or example-app/capacitor.config.json.',
19
- );
20
- process.exit(1);
21
- }
22
-
23
- const configText = readFileSync(configPath, 'utf8');
24
-
25
- function readConfigString(key, fallback) {
26
- const match = configText.match(new RegExp(`['"]?${key}['"]?\\s*[:=]\\s*['"]([^'"]+)['"]`));
27
- return match?.[1] ?? fallback;
28
- }
29
-
30
- function runCapgo(args, allowFailure = false) {
31
- const token = process.env.CAPGO_TOKEN;
32
- const fullArgs = ['@capgo/cli@8.5.3', ...args];
33
- if (token) {
34
- fullArgs.push('--apikey', token);
35
- }
36
- const result = spawnSync('bunx', fullArgs, {
37
- cwd: appDir,
38
- stdio: 'inherit',
39
- env: process.env,
40
- });
41
- if (!allowFailure && result.status !== 0) {
42
- process.exit(result.status ?? 1);
43
- }
44
- return result.status ?? 1;
45
- }
46
-
47
- const appId = process.env.CAPGO_APP_ID || readConfigString('appId');
48
- const appName = process.env.CAPGO_APP_NAME || readConfigString('appName', rootPackageJson.name);
49
- const webDir = process.env.CAPGO_WEB_DIR || readConfigString('webDir', 'dist');
50
- const distDir = resolve(appDir, webDir);
51
- const channel = process.env.CAPGO_CHANNEL || process.argv[2] || 'production';
52
- function usableVersion(version) {
53
- return version && version !== '0.0.0' ? version : undefined;
54
- }
55
-
56
- const bundle =
57
- process.env.CAPGO_BUNDLE || usableVersion(examplePackageJson.version) || usableVersion(rootPackageJson.version);
58
- const comment =
59
- process.env.CAPGO_COMMENT ||
60
- (process.env.GITHUB_RUN_NUMBER ? `${appName} run ${process.env.GITHUB_RUN_NUMBER}` : `${appName} ${bundle}`);
61
- const iconPath = process.env.CAPGO_ICON || resolve(appDir, 'assets', 'capgo-icon.png');
62
-
63
- if (!appId) {
64
- console.error('Missing Capgo app id. Set CAPGO_APP_ID or example-app/capacitor.config.* appId.');
65
- process.exit(1);
66
- }
67
-
68
- if (!existsSync(distDir)) {
69
- console.error(`Missing example app bundle at ${distDir}. Run bun run example:build first.`);
70
- process.exit(1);
71
- }
72
-
73
- if (!bundle) {
74
- console.error(
75
- 'Missing Capgo bundle version. Set CAPGO_BUNDLE or add a version to example-app/package.json or package.json.',
76
- );
77
- process.exit(1);
78
- }
79
-
80
- const appArgs = [appId, '--name', appName];
81
- if (existsSync(iconPath)) {
82
- appArgs.push('--icon', iconPath);
83
- }
84
-
85
- const setStatus = runCapgo(['app', 'set', ...appArgs], true);
86
- if (setStatus !== 0) {
87
- runCapgo(['app', 'add', ...appArgs]);
88
- }
89
-
90
- console.log(`Deploying ${appId} to Capgo channel "${channel}"`);
91
- const channelArgs = ['channel', 'add', channel, appId];
92
- if (channel === 'production') {
93
- channelArgs.push('--default');
94
- }
95
- runCapgo(channelArgs, true);
96
-
97
- runCapgo([
98
- 'bundle',
99
- 'upload',
100
- appId,
101
- '--bundle',
102
- bundle,
103
- '--path',
104
- webDir,
105
- '--channel',
106
- channel,
107
- '--package-json',
108
- 'package.json,../package.json',
109
- '--node-modules',
110
- 'node_modules,../node_modules',
111
- '--delta',
112
- '--no-key',
113
- '--ignore-checksum-check',
114
- '--version-exists-ok',
115
- '--comment',
116
- comment,
117
- ]);