Skip to content

Commit 104d673

Browse files
committed
Fixe #83: Replace pkijs with forge
1 parent ad89401 commit 104d673

File tree

5 files changed

+105
-210
lines changed

5 files changed

+105
-210
lines changed

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@
5454
"dependencies": {
5555
"@napi-rs/lzma": "^1.1.2",
5656
"andromatic": "^1.0.0",
57-
"asn1js": "^3.0.5",
5857
"bplist-creator": "^0.1.1",
5958
"bplist-parser": "^0.3.2",
6059
"cross-fetch": "^3.1.5",
@@ -64,9 +63,9 @@
6463
"fs-extra": "^11.1.0",
6564
"global-cache-dir": "^5.0.0",
6665
"ipa-extract-info": "^1.2.6",
66+
"node-forge": "^1.3.1",
6767
"node-ssh": "^13.1.0",
6868
"p-retry": "^5.1.2",
69-
"pkijs": "^3.0.14",
7069
"semver": "^7.3.8",
7170
"tempy": "^3.0.0",
7271
"ts-node": "^10.9.1",
@@ -80,6 +79,7 @@
8079
"@parcel/transformer-typescript-types": "2.8.2",
8180
"@types/fs-extra": "^11.0.0",
8281
"@types/node": "^18.11.18",
82+
"@types/node-forge": "^1.3.2",
8383
"@types/plist": "^3.0.2",
8484
"@types/promise-timeout": "^1.3.0",
8585
"@types/semver": "^7.3.13",

src/ios.ts

+31-31
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@ import frida from 'frida';
66
import { exists, mkdirp } from 'fs-extra';
77
import { readFile, writeFile } from 'fs/promises';
88
import globalCacheDir from 'global-cache-dir';
9+
import forge from 'node-forge';
910
import { NodeSSH } from 'node-ssh';
1011
import { join } from 'path';
11-
import { Certificate } from 'pkijs';
1212
import type { PlatformApi, PlatformApiOptions, Proxy, SupportedCapability, SupportedRunTarget } from '.';
1313
import { asyncUnimplemented, getObjFromFridaScript, isRecord, retryCondition } from './utils';
1414
import {
1515
arrayBufferToPem,
16+
asn1ValueToDer,
1617
certificateFingerprint,
1718
certificateHasExpired,
19+
createPkcs12Container,
1820
generateCertificate,
1921
pemToArrayBuffer,
2022
} from './utils/crypto';
@@ -165,53 +167,54 @@ export const iosApi = <RunTarget extends SupportedRunTarget<'ios'>>(
165167
if (!plist) throw new Error('Failed to ensure supervision mode: Invalid CloudConfiguration.');
166168

167169
let hostCert;
168-
let hostKey;
169170

170171
if (
171172
(await exists(join(cacheDir, 'ios', 'supervisorCert.pem'))) &&
172-
(await exists(join(cacheDir, 'ios', 'supervisorPrivateKey.pem')))
173+
(await exists(join(cacheDir, 'ios', 'supervisorKeyStore.p12')))
173174
) {
174-
hostCert = pemToArrayBuffer((await readFile(join(cacheDir, 'ios', 'supervisorCert.pem'))).toString());
175-
hostKey = pemToArrayBuffer(
176-
(await readFile(join(cacheDir, 'ios', 'supervisorPrivateKey.pem'))).toString()
177-
);
175+
hostCert = (await readFile(join(cacheDir, 'ios', 'supervisorCert.pem'))).toString();
178176

179177
if (!(await certificateHasExpired(hostCert))) {
180178
const hostCertFingerprint = await certificateFingerprint(hostCert);
181179

182-
// Test if the current host certificate is already controlling the device.
183-
if (
184-
plist.IsSupervised &&
185-
plist.SupervisorHostCertificates &&
186-
plist.SupervisorHostCertificates.length > 0 &&
187-
plist.SupervisorHostCertificates.some(
188-
async (cert) => (await certificateFingerprint(cert)) === hostCertFingerprint
180+
try {
181+
// Test if the current host certificate is already controlling the device.
182+
if (
183+
plist.IsSupervised &&
184+
plist.SupervisorHostCertificates &&
185+
plist.SupervisorHostCertificates.length > 0 &&
186+
plist.SupervisorHostCertificates.some(
187+
(cert) =>
188+
certificateFingerprint(arrayBufferToPem(cert, 'CERTIFICATE')) ===
189+
hostCertFingerprint
190+
)
189191
)
190-
)
191-
return;
192+
return;
193+
} catch (e) {
194+
// The certificate is invalid, so we need to generate a new one.
195+
hostCert = undefined;
196+
}
192197
} else {
193198
hostCert = undefined;
194-
hostKey = undefined;
195199
}
196200
}
197201

198-
if (!hostCert || !hostKey) {
202+
if (!hostCert) {
199203
// We have no exsiting keys, so let’s generate one.
200204
const generated = await generateCertificate(OrganizationName);
201205
hostCert = generated.certificate;
202-
hostKey = generated.privateKey;
206+
const hostKey = generated.privateKey;
207+
208+
const keyStore = createPkcs12Container(hostCert, hostKey, 'appstraction');
203209

204210
await mkdirp(join(cacheDir, 'ios'));
205-
await writeFile(join(cacheDir, 'ios', 'supervisorCert.pem'), arrayBufferToPem(hostCert, 'CERTIFICATE'));
206-
await writeFile(
207-
join(cacheDir, 'ios', 'supervisorPrivateKey.pem'),
208-
arrayBufferToPem(hostKey, 'PRIVATE KEY')
209-
);
211+
await writeFile(join(cacheDir, 'ios', 'supervisorCert.pem'), hostCert);
212+
await writeFile(join(cacheDir, 'ios', 'supervisorKeyStore.p12'), Buffer.from(keyStore.toHex(), 'hex'));
210213
}
211214

212215
const newPlist = {
213216
...plist,
214-
SupervisorHostCertificates: [Buffer.from(hostCert)],
217+
SupervisorHostCertificates: [Buffer.from(pemToArrayBuffer(hostCert))],
215218
IsSupervised: true,
216219
OrganizationName,
217220
AllowPairing: true,
@@ -419,15 +422,12 @@ export const iosApi = <RunTarget extends SupportedRunTarget<'ios'>>(
419422
throw new Error('SSH is required for installing a certificate authority.');
420423

421424
const certPem = await readFile(path, 'utf8');
425+
const certDer = Buffer.from(pemToArrayBuffer(certPem));
422426

423-
// A PEM certificate is just a base64-encoded DER certificate with a header and footer.
424-
const certBase64 = certPem.replace(/(-----(BEGIN|END) CERTIFICATE-----|[\r\n])/g, '');
425-
const certDer = Buffer.from(certBase64, 'base64');
426-
427-
const c = Certificate.fromBER(certDer);
427+
const c = forge.pki.certificateFromPem(certPem);
428428

429429
const sha256 = createHash('sha256').update(certDer).digest('hex');
430-
const subj = Buffer.from(c.subject.toSchema().valueBlock.toBER()).toString('hex');
430+
const subj = asn1ValueToDer(forge.pki.distinguishedNameToAsn1(c.subject)).toHex();
431431
const tset = Buffer.from(
432432
`<?xml version="1.0" encoding="UTF-8"?>
433433
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

src/types/forge.d.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type forge from 'node-forge';
2+
3+
declare module 'node-forge' {
4+
namespace pki {
5+
function distinguishedNameToAsn1(dn: forge.pki.Certificate['subject' | 'issuer']): forge.pki.Asn1;
6+
}
7+
}

src/utils/crypto.ts

+53-140
Original file line numberDiff line numberDiff line change
@@ -1,160 +1,64 @@
1-
import { BmpString, Integer } from 'asn1js';
2-
import { webcrypto } from 'crypto';
3-
import {
4-
AttributeTypeAndValue,
5-
AuthenticatedSafe,
6-
CertBag,
7-
Certificate,
8-
CryptoEngine,
9-
PFX,
10-
PKCS8ShroudedKeyBag,
11-
PrivateKeyInfo,
12-
SafeBag,
13-
SafeContents,
14-
setEngine,
15-
} from 'pkijs';
16-
17-
const crypto = new CryptoEngine({ name: 'node-webcrypto', crypto: webcrypto as Crypto });
18-
setEngine('node-webcrypto', crypto); // We need to do this, because there is a bug in pkijs (https://github.com/PeculiarVentures/PKI.js/issues/379)
1+
import forge from 'node-forge';
2+
const { pki, md } = forge;
193

204
export const generateCertificate = async (commonName: string, days?: number) => {
21-
const algorithm = crypto.getAlgorithmParameters('RSA-PSS', 'generateKey');
22-
const { privateKey, publicKey } = await crypto.generateKey(
23-
algorithm.algorithm as EcKeyAlgorithm,
24-
true,
25-
algorithm.usages
26-
);
5+
const keyPair = await new Promise<forge.pki.rsa.KeyPair>((res, rej) => {
6+
pki.rsa.generateKeyPair({ bits: 2048 }, (err, keyPair) => (err ? rej(err) : res(keyPair)));
7+
});
8+
const cert = pki.createCertificate();
279

28-
const cert = new Certificate();
10+
cert.publicKey = keyPair.publicKey;
2911
cert.version = 2;
30-
cert.serialNumber = new Integer({ value: Date.now() });
31-
cert.notBefore.value = new Date();
32-
cert.notAfter.value = new Date();
33-
cert.notAfter.value.setDate(cert.notBefore.value.getDate() + (days || 365));
12+
cert.serialNumber = Date.now().toString(10);
13+
cert.validity.notBefore = new Date();
14+
cert.validity.notAfter = new Date();
15+
cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + (days || 365));
3416

35-
cert.issuer.typesAndValues.push(
36-
new AttributeTypeAndValue({
37-
type: '2.5.4.3', // Common name
38-
value: new BmpString({ value: commonName }),
39-
})
40-
);
41-
cert.subject.typesAndValues.push(
42-
new AttributeTypeAndValue({
43-
type: '2.5.4.3', // Common name
44-
value: new BmpString({ value: commonName }),
45-
})
46-
);
17+
const attributes = [
18+
{
19+
name: 'commonName',
20+
value: commonName,
21+
},
22+
];
23+
cert.setSubject(attributes);
24+
cert.setIssuer(attributes);
4725

48-
await cert.subjectPublicKeyInfo.importKey(publicKey, crypto);
49-
await cert.sign(privateKey, 'SHA-256', crypto);
26+
cert.sign(keyPair.privateKey, md.sha256.create());
5027

5128
return {
52-
certificate: cert.toSchema().toBER(false),
53-
privateKey: await crypto.exportKey('pkcs8', privateKey),
29+
certificate: pki.certificateToPem(cert),
30+
privateKey: pki.privateKeyToPem(keyPair.privateKey),
5431
};
5532
};
5633

57-
export const certificateFingerprint = async (certificateBuffer: ArrayBuffer, hashAlgorithm?: 'SHA-256' | 'SHA-1') => {
58-
const certificate = await Certificate.fromBER(certificateBuffer);
59-
const hash = await crypto.digest(
60-
hashAlgorithm || 'SHA-256',
61-
certificate.subjectPublicKeyInfo.toSchema().toBER(false)
62-
);
63-
return Buffer.from(hash).toString('hex');
34+
export const certificateFingerprint = (certificatePem: string, hashAlgorithm?: 'SHA-256' | 'SHA-1') => {
35+
const cert = pki.certificateFromPem(certificatePem);
36+
return pki.getPublicKeyFingerprint(cert.publicKey, {
37+
type: 'SubjectPublicKeyInfo',
38+
md: hashAlgorithm === 'SHA-1' ? md.sha1.create() : md.sha256.create(),
39+
encoding: 'hex',
40+
});
6441
};
6542

66-
export const certificateHasExpired = async (certificateBuffer: ArrayBuffer) => {
67-
const certificate = await Certificate.fromBER(certificateBuffer);
68-
return certificate.notAfter.value < new Date();
43+
export const certificateHasExpired = (certificatePem: string) => {
44+
const cert = pki.certificateFromPem(certificatePem);
45+
return cert.validity.notAfter < new Date();
6946
};
7047

71-
export const createPkcs12Container = async (cert: ArrayBuffer, key: ArrayBuffer, password?: string) => {
72-
const encodedPassword = new TextEncoder().encode(password || '').buffer;
73-
74-
const pkcs12 = new PFX({
75-
parsedValue: {
76-
integrityMode: 0, // Password-Based Integrity Mode
77-
authenticatedSafe: new AuthenticatedSafe({
78-
parsedValue: {
79-
safeContents: [
80-
{
81-
privacyMode: 0, // 0 - No privacy mode
82-
value: new SafeContents({
83-
safeBags: [
84-
new SafeBag({
85-
bagId: '1.2.840.113549.1.12.10.1.2', // Shrouded Private Key Bag
86-
bagValue: new PKCS8ShroudedKeyBag({
87-
parsedValue: PrivateKeyInfo.fromBER(key),
88-
}),
89-
}),
90-
],
91-
}),
92-
},
93-
{
94-
privacyMode: 1, // 1 - Password based privacy mode,
95-
value: new SafeContents({
96-
safeBags: [
97-
new SafeBag({
98-
bagId: '1.2.840.113549.1.12.10.1.3', // Certificate bag
99-
bagValue: new CertBag({
100-
parsedValue: Certificate.fromBER(cert),
101-
}),
102-
}),
103-
],
104-
}),
105-
},
106-
],
107-
},
108-
}),
109-
},
110-
});
111-
112-
if (!pkcs12.parsedValue?.authenticatedSafe)
113-
throw new Error('Broken certificate container: pkcs12.parsedValue.authenticatedSafe is empty');
114-
115-
await pkcs12.parsedValue.authenticatedSafe.parsedValue.safeContents[0].value.safeBags[0].bagValue.makeInternalValues(
116-
{
117-
password: encodedPassword,
118-
contentEncryptionAlgorithm: {
119-
name: 'AES-CBC', // OpenSSL can only handle AES-CBC (https://github.com/PeculiarVentures/PKI.js/blob/469c403d102ee5149e8eb9ad19754c9696ed7c55/test/pkcs12SimpleExample.ts#L438)
120-
length: 128,
121-
},
122-
hmacHashAlgorithm: 'SHA-1', // OpenSSL can only handle SHA-1 (https://github.com/PeculiarVentures/PKI.js/blob/469c403d102ee5149e8eb9ad19754c9696ed7c55/test/pkcs12SimpleExample.ts#L441)
123-
iterationCount: 100000,
124-
},
125-
crypto
126-
);
127-
128-
pkcs12.parsedValue.authenticatedSafe.makeInternalValues(
129-
{
130-
safeContents: [
131-
{
132-
// Private key contents are encrypted differently, so this needs to be empty.
133-
},
134-
{
135-
password: encodedPassword,
136-
contentEncryptionAlgorithm: {
137-
name: 'AES-CBC',
138-
length: 128,
139-
},
140-
hmacHashAlgorithm: 'SHA-1',
141-
iterationCount: 100000,
142-
},
143-
],
144-
},
145-
crypto
48+
export const createPkcs12Container = (
49+
certPem: string,
50+
keyPem: string,
51+
password?: string,
52+
algorithm?: 'aes256' | '3des'
53+
) => {
54+
const p12 = forge.pkcs12.toPkcs12Asn1(
55+
pki.privateKeyFromPem(keyPem),
56+
pki.certificateFromPem(certPem),
57+
password || '',
58+
{ algorithm: algorithm || '3des' } // Apparently any sane algorithm is not supported by the typical ingestors (like go), so we default to 3des
14659
);
14760

148-
await pkcs12.makeInternalValues(
149-
{
150-
password: encodedPassword,
151-
iterations: 100000,
152-
pbkdf2HashAlgorithm: 'SHA-256',
153-
hmacHashAlgorithm: 'SHA-256',
154-
},
155-
crypto
156-
);
157-
return pkcs12.toSchema().toBER();
61+
return forge.asn1.toDer(p12);
15862
};
15963

16064
export const arrayBufferToPem = (buffer: ArrayBuffer, tag: 'CERTIFICATE' | 'PRIVATE KEY' | 'PUBLIC KEY') => {
@@ -169,3 +73,12 @@ export const pemToArrayBuffer = (pem: string) => {
16973
.replace(/\n/g, '');
17074
return Uint8Array.from(Buffer.from(base64, 'base64')).buffer;
17175
};
76+
77+
export const asn1ValueToDer = (asn1: forge.asn1.Asn1) => {
78+
if (typeof asn1.value === 'string' || !asn1.constructed) return forge.asn1.toDer(asn1);
79+
80+
return asn1.value.reduce((acc, cur) => {
81+
acc.putBuffer(forge.asn1.toDer(cur));
82+
return acc;
83+
}, forge.util.createBuffer());
84+
};

0 commit comments

Comments
 (0)