diff --git a/resources/signers.json b/resources/signers.json index f6db23b..3511ff1 100644 --- a/resources/signers.json +++ b/resources/signers.json @@ -1,6 +1,13 @@ { - "_comment": "Known APK signer identities. Detection matches any 'dnContains' substring (case-insensitive) against the signer's Subject or Issuer DN. Order matters — first match wins.", + "_comment": "Known APK signer identities. Detection matches any 'dnContains' substring (case-insensitive) against CN/O values in the signer's Subject or Issuer DN (OU and L are ignored to avoid false positives from lineage breadcrumbs). Order matters — first match wins, so list MORE SPECIFIC entries first.", "signers": [ + { + "id": "apc", + "label": "APC", + "fullName": "Automagic Package Changer", + "color": "#d400ff", + "dnContains": ["Automagic Package Changer"] + }, { "id": "vrp", "label": "VRP", @@ -15,13 +22,6 @@ "color": "#39ff14", "dnContains": ["NothingIsFree"] }, - { - "id": "apc", - "label": "APC", - "fullName": "Automagic Package Changer", - "color": "#d400ff", - "dnContains": ["Automagic Package Changer", "Automagic"] - }, { "id": "meta", "label": "Meta / Oculus", @@ -29,19 +29,19 @@ "color": "#00d4ff", "dnContains": ["Oculus VR", "Facebook Technologies", "Meta Platforms"] }, - { - "id": "google", - "label": "Google", - "fullName": "Google / Android", - "color": "#00d4ff", - "dnContains": ["CN=Android", "O=Google Inc."] - }, { "id": "debug", "label": "Debug Key", "fullName": "Android Debug", "color": "#888888", - "dnContains": ["CN=Android Debug", "O=Android"] + "dnContains": ["CN=Android Debug", "Android Debug"] + }, + { + "id": "google", + "label": "Google", + "fullName": "Google / Android", + "color": "#00d4ff", + "dnContains": ["O=Google Inc.", "O=Google LLC"] } ] } diff --git a/src/main/apk-inspector.js b/src/main/apk-inspector.js index 631f994..8c0c6ea 100644 --- a/src/main/apk-inspector.js +++ b/src/main/apk-inspector.js @@ -218,10 +218,20 @@ async function inspectSignature(apkPath) { ], 60000); const out = (stdout + '\n' + stderr); - // Scheme verification lines - if (/v1\s*scheme.*:\s*true/i.test(out)) result.schemes.v1 = true; - if (/v2\s*scheme.*:\s*true/i.test(out)) result.schemes.v2 = true; - if (/v3\s*scheme.*:\s*true/i.test(out)) result.schemes.v3 = true; + // Scheme verification lines. uber-apk-signer has used several formats across + // versions, so we match permissively. Examples we've seen: + // "v1 scheme: true" + // "signed by v1 scheme: true" + // "APK Signature Scheme v2: true" + // "scheme v3: true" + // "v2 (APK Signature Scheme v2): true" + const schemeRe = (n) => new RegExp( + '(?:scheme\\s*v' + n + '|v' + n + '\\s*\\(?\\s*(?:apk\\s*)?(?:signature\\s*)?scheme|v' + n + '\\s*scheme|v' + n + ')\\s*\\)?[^\\n]*?[:\\-]\\s*(true|verified|success)', + 'i' + ); + if (schemeRe(1).test(out)) result.schemes.v1 = true; + if (schemeRe(2).test(out)) result.schemes.v2 = true; + if (schemeRe(3).test(out)) result.schemes.v3 = true; // Per-signer certificate blocks // Typical uber-apk-signer --verbose block includes: @@ -244,6 +254,19 @@ async function inspectSignature(apkPath) { result.signed = result.signers.length > 0 || result.schemes.v1 || result.schemes.v2 || result.schemes.v3; + // If uber-apk-signer produced per-signer cert blocks but the scheme regex + // didn't pick anything up (output format drift), infer schemes from the + // overall verdict: a cert exists → at least v1 was verified. v2/v3 presence + // is detected from block text if available. + if (result.signed && !result.schemes.v1 && !result.schemes.v2 && !result.schemes.v3) { + // Check for any positive verification wording + if (/verified\s*:\s*true/i.test(out) || /signature\s+is\s+valid/i.test(out) || result.signers.length > 0) { + result.schemes.v1 = true; + if (/scheme\s*v2|v2\s*signature/i.test(out)) result.schemes.v2 = true; + if (/scheme\s*v3|v3\s*signature/i.test(out)) result.schemes.v3 = true; + } + } + // Identify first signer against the registry if (result.signers.length > 0) { const ident = identifySigner(result.signers[0]); diff --git a/src/main/main.js b/src/main/main.js index 17e62a0..b4c8bf1 100644 --- a/src/main/main.js +++ b/src/main/main.js @@ -5,12 +5,18 @@ const logger = require('./logger'); let mainWindow; +// Global UI scale. Applied via webContents.setZoomFactor so the entire page +// (title bar, content, modals) rescales cleanly. 0.78 shrinks the UI ~22% +// from the original design so the full app fits inside a default window on +// a 1080p screen without forcing the user to resize. +const APP_ZOOM_FACTOR = 0.78; + function createWindow() { mainWindow = new BrowserWindow({ - width: 900, - height: 680, - minWidth: 750, - minHeight: 580, + width: 820, + height: 760, + minWidth: 640, + minHeight: 520, backgroundColor: '#0d0221', icon: path.join(__dirname, '../../icon.png'), frame: false, @@ -19,10 +25,18 @@ function createWindow() { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, - sandbox: false + sandbox: false, + zoomFactor: APP_ZOOM_FACTOR } }); + // Belt-and-suspenders: enforce the zoom factor after the page loads too. + // Chromium occasionally resets zoom on reload or when the page completes + // first paint, so re-apply on did-finish-load. + mainWindow.webContents.on('did-finish-load', () => { + mainWindow.webContents.setZoomFactor(APP_ZOOM_FACTOR); + }); + mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')); // Open DevTools in development diff --git a/src/main/signer-identity.js b/src/main/signer-identity.js index fed9489..78520de 100644 --- a/src/main/signer-identity.js +++ b/src/main/signer-identity.js @@ -29,12 +29,52 @@ function loadRegistry() { } /** - * Match a signer DN (subject and/or issuer combined) against the registry. + * Extract a named RDN value out of a DN string. Case-insensitive key match. + * Example: extractRdn("CN=Foo, OU=Bar, O=Baz", "O") -> "Baz" + */ +function extractRdn(dn, key) { + if (!dn) return null; + const re = new RegExp('(?:^|,)\\s*' + key + '\\s*=\\s*([^,]+)', 'i'); + const m = dn.match(re); + return m ? m[1].trim() : null; +} + +/** + * Pull the CN value out of a DN string like "CN=Foo, OU=Bar, O=Baz". + */ +function extractCN(dn) { + return extractRdn(dn, 'CN'); +} + +/** + * Build a lookup string from CN + O + also raw CN=/O= fragments so registry + * entries can match either by value or by a `CN=...` / `O=...` literal. + * + * IMPORTANT: We deliberately EXCLUDE OU and L fields. OU is user-configurable + * signature text ("DMP used Automagic on this APK!") and L carries our lineage + * breadcrumb ("Previously signed by NothingIsFree (NIF)"). Matching those + * would cause false positives — e.g. APC's own cert would match NIF because + * "NothingIsFree" is in the L= lineage line. + */ +function buildIdentityHaystack(dn) { + if (!dn) return ''; + const cn = extractRdn(dn, 'CN') || ''; + const o = extractRdn(dn, 'O') || ''; + const parts = []; + if (cn) { parts.push(cn); parts.push('CN=' + cn); } + if (o) { parts.push(o); parts.push('O=' + o); } + return parts.join(' ').toLowerCase(); +} + +/** + * Match a signer DN (subject and/or issuer) against the registry. * Returns the matched signer record or null. + * + * Only CN and O fields participate in matching — see buildIdentityHaystack. */ function identifySigner({ subject, issuer }) { - const haystack = ((subject || '') + ' ' + (issuer || '')).toLowerCase(); - if (!haystack.trim()) return null; + const haystack = (buildIdentityHaystack(subject) + ' ' + buildIdentityHaystack(issuer)).trim(); + if (!haystack) return null; const registry = loadRegistry(); for (const entry of registry) { @@ -47,13 +87,4 @@ function identifySigner({ subject, issuer }) { return null; } -/** - * Pull the CN value out of a DN string like "CN=Foo, OU=Bar, O=Baz". - */ -function extractCN(dn) { - if (!dn) return null; - const m = dn.match(/CN=([^,]+)/i); - return m ? m[1].trim() : null; -} - module.exports = { identifySigner, extractCN, loadRegistry }; diff --git a/src/renderer/index.html b/src/renderer/index.html index 85920c5..47daaa5 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -107,6 +107,10 @@