Skip to content

Commit

Permalink
Fixe #83: Replace pkijs with forge
Browse files Browse the repository at this point in the history
  • Loading branch information
zner0L committed Jun 1, 2023
1 parent ad89401 commit 104d673
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 210 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
"dependencies": {
"@napi-rs/lzma": "^1.1.2",
"andromatic": "^1.0.0",
"asn1js": "^3.0.5",
"bplist-creator": "^0.1.1",
"bplist-parser": "^0.3.2",
"cross-fetch": "^3.1.5",
Expand All @@ -64,9 +63,9 @@
"fs-extra": "^11.1.0",
"global-cache-dir": "^5.0.0",
"ipa-extract-info": "^1.2.6",
"node-forge": "^1.3.1",
"node-ssh": "^13.1.0",
"p-retry": "^5.1.2",
"pkijs": "^3.0.14",
"semver": "^7.3.8",
"tempy": "^3.0.0",
"ts-node": "^10.9.1",
Expand All @@ -80,6 +79,7 @@
"@parcel/transformer-typescript-types": "2.8.2",
"@types/fs-extra": "^11.0.0",
"@types/node": "^18.11.18",
"@types/node-forge": "^1.3.2",
"@types/plist": "^3.0.2",
"@types/promise-timeout": "^1.3.0",
"@types/semver": "^7.3.13",
Expand Down
62 changes: 31 additions & 31 deletions src/ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ import frida from 'frida';
import { exists, mkdirp } from 'fs-extra';
import { readFile, writeFile } from 'fs/promises';
import globalCacheDir from 'global-cache-dir';
import forge from 'node-forge';
import { NodeSSH } from 'node-ssh';
import { join } from 'path';
import { Certificate } from 'pkijs';
import type { PlatformApi, PlatformApiOptions, Proxy, SupportedCapability, SupportedRunTarget } from '.';
import { asyncUnimplemented, getObjFromFridaScript, isRecord, retryCondition } from './utils';
import {
arrayBufferToPem,
asn1ValueToDer,
certificateFingerprint,
certificateHasExpired,
createPkcs12Container,
generateCertificate,
pemToArrayBuffer,
} from './utils/crypto';
Expand Down Expand Up @@ -165,53 +167,54 @@ export const iosApi = <RunTarget extends SupportedRunTarget<'ios'>>(
if (!plist) throw new Error('Failed to ensure supervision mode: Invalid CloudConfiguration.');

let hostCert;
let hostKey;

if (
(await exists(join(cacheDir, 'ios', 'supervisorCert.pem'))) &&
(await exists(join(cacheDir, 'ios', 'supervisorPrivateKey.pem')))
(await exists(join(cacheDir, 'ios', 'supervisorKeyStore.p12')))
) {
hostCert = pemToArrayBuffer((await readFile(join(cacheDir, 'ios', 'supervisorCert.pem'))).toString());
hostKey = pemToArrayBuffer(
(await readFile(join(cacheDir, 'ios', 'supervisorPrivateKey.pem'))).toString()
);
hostCert = (await readFile(join(cacheDir, 'ios', 'supervisorCert.pem'))).toString();

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

// Test if the current host certificate is already controlling the device.
if (
plist.IsSupervised &&
plist.SupervisorHostCertificates &&
plist.SupervisorHostCertificates.length > 0 &&
plist.SupervisorHostCertificates.some(
async (cert) => (await certificateFingerprint(cert)) === hostCertFingerprint
try {
// Test if the current host certificate is already controlling the device.
if (
plist.IsSupervised &&
plist.SupervisorHostCertificates &&
plist.SupervisorHostCertificates.length > 0 &&
plist.SupervisorHostCertificates.some(
(cert) =>
certificateFingerprint(arrayBufferToPem(cert, 'CERTIFICATE')) ===
hostCertFingerprint
)
)
)
return;
return;
} catch (e) {
// The certificate is invalid, so we need to generate a new one.
hostCert = undefined;
}
} else {
hostCert = undefined;
hostKey = undefined;
}
}

if (!hostCert || !hostKey) {
if (!hostCert) {
// We have no exsiting keys, so let’s generate one.
const generated = await generateCertificate(OrganizationName);
hostCert = generated.certificate;
hostKey = generated.privateKey;
const hostKey = generated.privateKey;

const keyStore = createPkcs12Container(hostCert, hostKey, 'appstraction');

await mkdirp(join(cacheDir, 'ios'));
await writeFile(join(cacheDir, 'ios', 'supervisorCert.pem'), arrayBufferToPem(hostCert, 'CERTIFICATE'));
await writeFile(
join(cacheDir, 'ios', 'supervisorPrivateKey.pem'),
arrayBufferToPem(hostKey, 'PRIVATE KEY')
);
await writeFile(join(cacheDir, 'ios', 'supervisorCert.pem'), hostCert);
await writeFile(join(cacheDir, 'ios', 'supervisorKeyStore.p12'), Buffer.from(keyStore.toHex(), 'hex'));
}

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

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

// A PEM certificate is just a base64-encoded DER certificate with a header and footer.
const certBase64 = certPem.replace(/(-----(BEGIN|END) CERTIFICATE-----|[\r\n])/g, '');
const certDer = Buffer.from(certBase64, 'base64');

const c = Certificate.fromBER(certDer);
const c = forge.pki.certificateFromPem(certPem);

const sha256 = createHash('sha256').update(certDer).digest('hex');
const subj = Buffer.from(c.subject.toSchema().valueBlock.toBER()).toString('hex');
const subj = asn1ValueToDer(forge.pki.distinguishedNameToAsn1(c.subject)).toHex();
const tset = Buffer.from(
`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
Expand Down
7 changes: 7 additions & 0 deletions src/types/forge.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type forge from 'node-forge';

declare module 'node-forge' {
namespace pki {
function distinguishedNameToAsn1(dn: forge.pki.Certificate['subject' | 'issuer']): forge.pki.Asn1;
}
}
193 changes: 53 additions & 140 deletions src/utils/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,160 +1,64 @@
import { BmpString, Integer } from 'asn1js';
import { webcrypto } from 'crypto';
import {
AttributeTypeAndValue,
AuthenticatedSafe,
CertBag,
Certificate,
CryptoEngine,
PFX,
PKCS8ShroudedKeyBag,
PrivateKeyInfo,
SafeBag,
SafeContents,
setEngine,
} from 'pkijs';

const crypto = new CryptoEngine({ name: 'node-webcrypto', crypto: webcrypto as Crypto });
setEngine('node-webcrypto', crypto); // We need to do this, because there is a bug in pkijs (https://github.com/PeculiarVentures/PKI.js/issues/379)
import forge from 'node-forge';
const { pki, md } = forge;

export const generateCertificate = async (commonName: string, days?: number) => {
const algorithm = crypto.getAlgorithmParameters('RSA-PSS', 'generateKey');
const { privateKey, publicKey } = await crypto.generateKey(
algorithm.algorithm as EcKeyAlgorithm,
true,
algorithm.usages
);
const keyPair = await new Promise<forge.pki.rsa.KeyPair>((res, rej) => {
pki.rsa.generateKeyPair({ bits: 2048 }, (err, keyPair) => (err ? rej(err) : res(keyPair)));
});
const cert = pki.createCertificate();

const cert = new Certificate();
cert.publicKey = keyPair.publicKey;
cert.version = 2;
cert.serialNumber = new Integer({ value: Date.now() });
cert.notBefore.value = new Date();
cert.notAfter.value = new Date();
cert.notAfter.value.setDate(cert.notBefore.value.getDate() + (days || 365));
cert.serialNumber = Date.now().toString(10);
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + (days || 365));

cert.issuer.typesAndValues.push(
new AttributeTypeAndValue({
type: '2.5.4.3', // Common name
value: new BmpString({ value: commonName }),
})
);
cert.subject.typesAndValues.push(
new AttributeTypeAndValue({
type: '2.5.4.3', // Common name
value: new BmpString({ value: commonName }),
})
);
const attributes = [
{
name: 'commonName',
value: commonName,
},
];
cert.setSubject(attributes);
cert.setIssuer(attributes);

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

return {
certificate: cert.toSchema().toBER(false),
privateKey: await crypto.exportKey('pkcs8', privateKey),
certificate: pki.certificateToPem(cert),
privateKey: pki.privateKeyToPem(keyPair.privateKey),
};
};

export const certificateFingerprint = async (certificateBuffer: ArrayBuffer, hashAlgorithm?: 'SHA-256' | 'SHA-1') => {
const certificate = await Certificate.fromBER(certificateBuffer);
const hash = await crypto.digest(
hashAlgorithm || 'SHA-256',
certificate.subjectPublicKeyInfo.toSchema().toBER(false)
);
return Buffer.from(hash).toString('hex');
export const certificateFingerprint = (certificatePem: string, hashAlgorithm?: 'SHA-256' | 'SHA-1') => {
const cert = pki.certificateFromPem(certificatePem);
return pki.getPublicKeyFingerprint(cert.publicKey, {
type: 'SubjectPublicKeyInfo',
md: hashAlgorithm === 'SHA-1' ? md.sha1.create() : md.sha256.create(),
encoding: 'hex',
});
};

export const certificateHasExpired = async (certificateBuffer: ArrayBuffer) => {
const certificate = await Certificate.fromBER(certificateBuffer);
return certificate.notAfter.value < new Date();
export const certificateHasExpired = (certificatePem: string) => {
const cert = pki.certificateFromPem(certificatePem);
return cert.validity.notAfter < new Date();
};

export const createPkcs12Container = async (cert: ArrayBuffer, key: ArrayBuffer, password?: string) => {
const encodedPassword = new TextEncoder().encode(password || '').buffer;

const pkcs12 = new PFX({
parsedValue: {
integrityMode: 0, // Password-Based Integrity Mode
authenticatedSafe: new AuthenticatedSafe({
parsedValue: {
safeContents: [
{
privacyMode: 0, // 0 - No privacy mode
value: new SafeContents({
safeBags: [
new SafeBag({
bagId: '1.2.840.113549.1.12.10.1.2', // Shrouded Private Key Bag
bagValue: new PKCS8ShroudedKeyBag({
parsedValue: PrivateKeyInfo.fromBER(key),
}),
}),
],
}),
},
{
privacyMode: 1, // 1 - Password based privacy mode,
value: new SafeContents({
safeBags: [
new SafeBag({
bagId: '1.2.840.113549.1.12.10.1.3', // Certificate bag
bagValue: new CertBag({
parsedValue: Certificate.fromBER(cert),
}),
}),
],
}),
},
],
},
}),
},
});

if (!pkcs12.parsedValue?.authenticatedSafe)
throw new Error('Broken certificate container: pkcs12.parsedValue.authenticatedSafe is empty');

await pkcs12.parsedValue.authenticatedSafe.parsedValue.safeContents[0].value.safeBags[0].bagValue.makeInternalValues(
{
password: encodedPassword,
contentEncryptionAlgorithm: {
name: 'AES-CBC', // OpenSSL can only handle AES-CBC (https://github.com/PeculiarVentures/PKI.js/blob/469c403d102ee5149e8eb9ad19754c9696ed7c55/test/pkcs12SimpleExample.ts#L438)
length: 128,
},
hmacHashAlgorithm: 'SHA-1', // OpenSSL can only handle SHA-1 (https://github.com/PeculiarVentures/PKI.js/blob/469c403d102ee5149e8eb9ad19754c9696ed7c55/test/pkcs12SimpleExample.ts#L441)
iterationCount: 100000,
},
crypto
);

pkcs12.parsedValue.authenticatedSafe.makeInternalValues(
{
safeContents: [
{
// Private key contents are encrypted differently, so this needs to be empty.
},
{
password: encodedPassword,
contentEncryptionAlgorithm: {
name: 'AES-CBC',
length: 128,
},
hmacHashAlgorithm: 'SHA-1',
iterationCount: 100000,
},
],
},
crypto
export const createPkcs12Container = (
certPem: string,
keyPem: string,
password?: string,
algorithm?: 'aes256' | '3des'
) => {
const p12 = forge.pkcs12.toPkcs12Asn1(
pki.privateKeyFromPem(keyPem),
pki.certificateFromPem(certPem),
password || '',
{ algorithm: algorithm || '3des' } // Apparently any sane algorithm is not supported by the typical ingestors (like go), so we default to 3des
);

await pkcs12.makeInternalValues(
{
password: encodedPassword,
iterations: 100000,
pbkdf2HashAlgorithm: 'SHA-256',
hmacHashAlgorithm: 'SHA-256',
},
crypto
);
return pkcs12.toSchema().toBER();
return forge.asn1.toDer(p12);
};

export const arrayBufferToPem = (buffer: ArrayBuffer, tag: 'CERTIFICATE' | 'PRIVATE KEY' | 'PUBLIC KEY') => {
Expand All @@ -169,3 +73,12 @@ export const pemToArrayBuffer = (pem: string) => {
.replace(/\n/g, '');
return Uint8Array.from(Buffer.from(base64, 'base64')).buffer;
};

export const asn1ValueToDer = (asn1: forge.asn1.Asn1) => {
if (typeof asn1.value === 'string' || !asn1.constructed) return forge.asn1.toDer(asn1);

return asn1.value.reduce((acc, cur) => {
acc.putBuffer(forge.asn1.toDer(cur));
return acc;
}, forge.util.createBuffer());
};
Loading

0 comments on commit 104d673

Please sign in to comment.