Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/codesigning-plugin-auto-pubkey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@callstack/repack": minor
---

Add `publicKeyPath` and `nativeProjectPaths` options to `CodeSigningPlugin`. When `publicKeyPath` is set, the plugin automatically embeds the public key into `Info.plist` (iOS) and `strings.xml` (Android) during compilation, removing the need for manual native file setup. The `embedPublicKey` utility is also exported for standalone use.
68 changes: 66 additions & 2 deletions packages/repack/src/plugins/CodeSigningPlugin/CodeSigningPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Compiler as RspackCompiler } from '@rspack/core';
import jwt from 'jsonwebtoken';
import type { Compiler as WebpackCompiler } from 'webpack';
import { type CodeSigningPluginConfig, validateConfig } from './config.js';
import { embedPublicKey } from './embedPublicKey.js';

export class CodeSigningPlugin {
private chunkFilenames: Set<string>;
Expand All @@ -26,7 +27,6 @@ export class CodeSigningPlugin {
mainOutputFilename: string,
excludedChunks: string[] | RegExp[]
): boolean {
/** Exclude non-chunks & main chunk as it's always local */
if (!this.chunkFilenames.has(file) || file === mainOutputFilename) {
return false;
}
Expand All @@ -39,6 +39,68 @@ export class CodeSigningPlugin {
});
}

private embedPublicKeyInNativeProjects(compiler: RspackCompiler) {
if (!this.config.publicKeyPath) {
return;
}

const logger = compiler.getInfrastructureLogger('RepackCodeSigningPlugin');
const projectRoot = compiler.context;

const publicKeyPath = path.isAbsolute(this.config.publicKeyPath)
? this.config.publicKeyPath
: path.resolve(projectRoot, this.config.publicKeyPath);

if (!fs.existsSync(publicKeyPath)) {
logger.warn(
`Public key not found at ${publicKeyPath}. ` +
'Skipping automatic embedding into native project files.'
);
return;
}

const result = embedPublicKey({
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we would want to think of a way to not always try to embed, like some kind of cache or something if we know it was already embedded? Maybe not worth the effort though, what do you think?

publicKeyPath,
projectRoot,
iosInfoPlistPath: this.config.nativeProjectPaths?.ios
? path.isAbsolute(this.config.nativeProjectPaths.ios)
Comment thread
bartekkrok marked this conversation as resolved.
Outdated
? this.config.nativeProjectPaths.ios
: path.resolve(projectRoot, this.config.nativeProjectPaths.ios)
: undefined,
androidStringsXmlPath: this.config.nativeProjectPaths?.android
? path.isAbsolute(this.config.nativeProjectPaths.android)
? this.config.nativeProjectPaths.android
: path.resolve(projectRoot, this.config.nativeProjectPaths.android)
: undefined,
});

if (result.ios.modified) {
logger.info(`Embedded public key in iOS Info.plist: ${result.ios.path}`);
} else if (result.ios.error) {
logger.warn(`Failed to embed public key in iOS: ${result.ios.error}`);
} else {
logger.warn(
'Could not find iOS Info.plist. Skipping auto-embedding for iOS. ' +
'Use nativeProjectPaths.ios or manually add the public key to Info.plist.'
);
}

if (result.android.modified) {
logger.info(
`Embedded public key in Android strings.xml: ${result.android.path}`
);
} else if (result.android.error) {
logger.warn(
`Failed to embed public key in Android: ${result.android.error}`
);
} else {
logger.warn(
'Could not find Android strings.xml. Skipping auto-embedding for Android. ' +
'Use nativeProjectPaths.android or manually add the public key to strings.xml.'
);
}
}

apply(compiler: RspackCompiler): void;
apply(compiler: WebpackCompiler): void;

Expand All @@ -62,7 +124,7 @@ export class CodeSigningPlugin {
*/
const TOKEN_BUFFER_SIZE = 1280;
/**
* Used to denote beginning of the code-signing section of the bundle
* Used to denote the beginning of the code-signing section of the bundle
* alias for "Repack Code-Signing Signature Begin"
*/
const BEGIN_CS_MARK = '/* RCSSB */';
Expand All @@ -72,6 +134,8 @@ export class CodeSigningPlugin {
: path.resolve(compiler.context, this.config.privateKeyPath);
const privateKey = fs.readFileSync(privateKeyPath);

this.embedPublicKeyInNativeProjects(compiler);

const excludedChunks = Array.isArray(this.config.excludeChunks)
? this.config.excludeChunks
: [this.config.excludeChunks as RegExp];
Expand Down
27 changes: 27 additions & 0 deletions packages/repack/src/plugins/CodeSigningPlugin/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@ export interface CodeSigningPluginConfig {
privateKeyPath: string;
/** Names of chunks to exclude from being signed. */
excludeChunks?: string[] | RegExp | RegExp[];
/**
* Path to the public key file. When provided, the plugin will automatically
* embed the public key into native project files (Info.plist for iOS,
* strings.xml for Android) so that the runtime can verify signed bundles.
*
* Relative paths are resolved from the project root (compiler context).
*/
publicKeyPath?: string;
/**
* Override auto-detected paths to native project files where the public key
* should be embedded. Only used when `publicKeyPath` is set.
*/
nativeProjectPaths?: {
Comment thread
dannyhw marked this conversation as resolved.
/** Path to iOS Info.plist. Auto-detected if not provided. */
ios?: string;
/** Path to Android strings.xml. Auto-detected if not provided. */
android?: string;
};
}

type Schema = Parameters<typeof validate>[0];
Expand Down Expand Up @@ -38,6 +56,15 @@ export const optionsSchema: Schema = {
},
],
},
publicKeyPath: { type: 'string' },
nativeProjectPaths: {
type: 'object',
properties: {
ios: { type: 'string' },
android: { type: 'string' },
},
additionalProperties: false,
},
},
required: ['privateKeyPath'],
additionalProperties: false,
Expand Down
200 changes: 200 additions & 0 deletions packages/repack/src/plugins/CodeSigningPlugin/embedPublicKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import fs from 'node:fs';
import path from 'node:path';

export interface EmbedPublicKeyConfig {
/** Absolute path to the public key file. */
publicKeyPath: string;
/** Absolute path to the project root. */
projectRoot: string;
/** Custom path to iOS Info.plist. Auto-detected if not provided. */
iosInfoPlistPath?: string;
/** Custom path to Android strings.xml. Auto-detected if not provided. */
androidStringsXmlPath?: string;
}

export interface EmbedPublicKeyResult {
ios: { modified: boolean; path?: string; error?: string };
android: { modified: boolean; path?: string; error?: string };
}

/**
* Embeds the Re.Pack code-signing public key into native project files.
* Modifies `Info.plist` (iOS) and `strings.xml` (Android) so the runtime
* can verify signed bundles without manual file editing.
*/
export function embedPublicKey(config: EmbedPublicKeyConfig): EmbedPublicKeyResult {
const publicKey = fs.readFileSync(config.publicKeyPath, 'utf-8').trim();
Comment thread
bartekkrok marked this conversation as resolved.
Outdated

const result: EmbedPublicKeyResult = {
ios: { modified: false },
android: { modified: false },
};

const plistPath =
config.iosInfoPlistPath ??
findIOSInfoPlistPath(config.projectRoot);

if (plistPath) {
try {
embedPublicKeyInPlist(publicKey, plistPath);
result.ios = { modified: true, path: plistPath };
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
result.ios = { modified: false, path: plistPath, error: message };
}
}

const stringsXmlPath =
config.androidStringsXmlPath ??
findAndroidStringsXmlPath(config.projectRoot);

if (stringsXmlPath) {
try {
embedPublicKeyInStringsXml(publicKey, stringsXmlPath);
Comment thread
bartekkrok marked this conversation as resolved.
result.android = { modified: true, path: stringsXmlPath };
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
result.android = { modified: false, path: stringsXmlPath, error: message };
}
}

return result;
}

/**
* Searches for `Info.plist` inside `ios/<AppDir>/Info.plist`.
* Returns the first match or `null`.
*/
export function findIOSInfoPlistPath(projectRoot: string): string | null {
const iosDir = path.join(projectRoot, 'ios');
if (!fs.existsSync(iosDir)) {
return null;
}

let entries: fs.Dirent[];
try {
entries = fs.readdirSync(iosDir, { withFileTypes: true });
} catch {
return null;
}

for (const entry of entries) {
if (!entry.isDirectory()) continue;
// Skip common non-app directories
if (entry.name === 'Pods' || entry.name === 'build' || entry.name.endsWith('.xcodeproj') || entry.name.endsWith('.xcworkspace')) {
continue;
}
const plistPath = path.join(iosDir, entry.name, 'Info.plist');
if (fs.existsSync(plistPath)) {
return plistPath;
}
}

return null;
}

/**
* Returns the standard path to `strings.xml` if it exists, or `null`.
*/
export function findAndroidStringsXmlPath(projectRoot: string): string | null {
const stringsPath = path.join(
projectRoot,
'android',
'app',
'src',
'main',
'res',
'values',
'strings.xml'
);
return fs.existsSync(stringsPath) ? stringsPath : null;
}

/**
* Embeds or updates `RepackPublicKey` in an iOS `Info.plist` file.
*/
export function embedPublicKeyInPlist(
publicKey: string,
plistPath: string
): void {
let content = fs.readFileSync(plistPath, 'utf-8');

const existingKeyPattern =
/[ \t]*<key>RepackPublicKey<\/key>\s*<string>[\s\S]*?<\/string>/;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if regex is the right move here

fast-xml-parser and plist or @plist/parse / @plist/plist

could probably be used to be more accurate/robust.

what do you think?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the tradeoff would be plist parsing could be slower


const replacement =
'\t<key>RepackPublicKey</key>\n' +
`\t<string>${escapeXml(publicKey)}</string>`;

if (existingKeyPattern.test(content)) {
content = content.replace(existingKeyPattern, replacement);
} else {
const insertIdx = content.lastIndexOf('</dict>');
if (insertIdx === -1) {
throw new Error(
`[CodeSigningPlugin] Could not find </dict> in ${plistPath}. ` +
'The file may not be a valid Info.plist.'
);
}
content =
content.slice(0, insertIdx) +
replacement +
'\n' +
content.slice(insertIdx);
}

fs.writeFileSync(plistPath, content, 'utf-8');
}

/**
* Embeds or updates `RepackPublicKey` in an Android `strings.xml` file.
* Creates the file if it does not exist.
*/
export function embedPublicKeyInStringsXml(
publicKey: string,
stringsXmlPath: string
): void {
const escapedKey = escapeXml(publicKey);
const newEntry = ` <string name="RepackPublicKey" translatable="false">${escapedKey}</string>`;

if (!fs.existsSync(stringsXmlPath)) {
const dir = path.dirname(stringsXmlPath);
fs.mkdirSync(dir, { recursive: true });
const content =
'<?xml version="1.0" encoding="utf-8"?>\n' +
'<resources>\n' +
newEntry +
'\n' +
'</resources>\n';
fs.writeFileSync(stringsXmlPath, content, 'utf-8');
return;
}

let content = fs.readFileSync(stringsXmlPath, 'utf-8');

const existingPattern =
/[ \t]*<string name="RepackPublicKey"[^>]*>[\s\S]*?<\/string>/;

if (existingPattern.test(content)) {
content = content.replace(existingPattern, newEntry);
} else {
const insertIdx = content.lastIndexOf('</resources>');
if (insertIdx === -1) {
throw new Error(
`[CodeSigningPlugin] Could not find </resources> in ${stringsXmlPath}. ` +
'The file may not be a valid strings.xml.'
);
}
content =
content.slice(0, insertIdx) +
newEntry +
'\n' +
content.slice(insertIdx);
}

fs.writeFileSync(stringsXmlPath, content, 'utf-8');
}

function escapeXml(str: string): string {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
5 changes: 5 additions & 0 deletions packages/repack/src/plugins/CodeSigningPlugin/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
export { CodeSigningPlugin } from './CodeSigningPlugin.js';
export type { CodeSigningPluginConfig } from './config.js';
export { embedPublicKey } from './embedPublicKey.js';
export type {
EmbedPublicKeyConfig,
EmbedPublicKeyResult,
} from './embedPublicKey.js';
Loading
Loading