Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
32 changes: 16 additions & 16 deletions resources/signers.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -15,33 +22,26 @@
"color": "#39ff14",
"dnContains": ["NothingIsFree"]
},
{
"id": "apc",
"label": "APC",
"fullName": "Automagic Package Changer",
"color": "#d400ff",
"dnContains": ["Automagic Package Changer", "Automagic"]
},
{
"id": "meta",
"label": "Meta / Oculus",
"fullName": "Oculus VR, LLC",
"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"]
}
]
}
31 changes: 27 additions & 4 deletions src/main/apk-inspector.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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]);
Expand Down
24 changes: 19 additions & 5 deletions src/main/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
55 changes: 43 additions & 12 deletions src/main/signer-identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 };
4 changes: 4 additions & 0 deletions src/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ <h1 class="app-title">
<!-- Info Panel (shown after APK is loaded) -->
<div class="info-panel hidden" id="infoPanel">
<div class="neon-pipe-horizontal short"></div>
<div class="already-tagged-banner hidden" id="alreadyTaggedBanner">
<span class="atb-icon">&#x26A0;</span>
<span class="atb-text">ALREADY TAGGED — <span id="atbDetail">this APK has been renamed before</span></span>
</div>
<div class="info-grid">
<div class="info-item">
<span class="info-label">ORIGINAL PKG:</span>
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/scripts/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
packageName,
filePath,
fileName,
obbFound: obbCheck.found
obbFound: obbCheck.found,
signerIdentity: info.signature && info.signature.identity
});

if (window.InfoModal) window.InfoModal.setInfo(info);
Expand Down
38 changes: 38 additions & 0 deletions src/renderer/scripts/ui-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,41 @@
const newPkg = document.getElementById('newPkg');
const obbStatus = document.getElementById('obbStatus');
const fileName = document.getElementById('fileName');
const alreadyTaggedBanner = document.getElementById('alreadyTaggedBanner');
const atbDetail = document.getElementById('atbDetail');

// Package segments that indicate the APK has already been tagged by APC
// or a sibling tool. Used to raise the proactive warning banner on drop.
const KNOWN_TAG_SEGMENTS = ['apc', 'mr', 'dmp', 'mrfix'];

function detectExistingTag(packageName, signerIdentity) {
if (!packageName) return null;
const segs = packageName.split('.');
// Look at the second segment (our insertion point) and any subsequent matches
const tagSegs = segs.filter(s => KNOWN_TAG_SEGMENTS.includes(s.toLowerCase()));
if (tagSegs.length > 0) {
return { reason: 'package', tag: tagSegs[0] };
}
if (signerIdentity && signerIdentity.id === 'apc') {
return { reason: 'signer', tag: null };
}
return null;
}

function renderAlreadyTagged(packageName, signerIdentity) {
if (!alreadyTaggedBanner) return;
const hit = detectExistingTag(packageName, signerIdentity);
if (!hit) {
alreadyTaggedBanner.classList.add('hidden');
return;
}
if (hit.reason === 'signer') {
atbDetail.textContent = 'this APK is already signed by APC — renaming again will stack another tag';
} else {
atbDetail.textContent = `package contains ".${hit.tag}" — renaming will stack another tag`;
}
alreadyTaggedBanner.classList.remove('hidden');
}
const btnRename = document.getElementById('btnRename');
const actionSection = document.getElementById('actionSection');
const progressSection = document.getElementById('progressSection');
Expand Down Expand Up @@ -134,6 +169,8 @@
obbStatus.textContent = info.obbFound ? 'Found - will be renamed' : 'Not found nearby';
obbStatus.style.color = info.obbFound ? '#00ff41' : '#666';

renderAlreadyTagged(info.packageName, info.signerIdentity);

infoPanel.classList.remove('hidden');
updateNewPackagePreview();
validateState();
Expand Down Expand Up @@ -164,6 +201,7 @@
currentPackageName = null;
currentFilePath = null;
infoPanel.classList.add('hidden');
if (alreadyTaggedBanner) alreadyTaggedBanner.classList.add('hidden');
progressSection.classList.add('hidden');
resultSection.classList.add('hidden');
actionSection.style.display = 'flex';
Expand Down
10 changes: 5 additions & 5 deletions src/renderer/styles/drop-zone.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
.drop-zone {
position: relative;
border: 2px dashed rgba(0, 255, 65, 0.3);
padding: 32px;
padding: 22px;
text-align: center;
cursor: pointer;
transition: all 0.4s ease;
background: rgba(13, 2, 33, 0.6);
min-height: 160px;
min-height: 130px;
display: flex;
align-items: center;
justify-content: center;
Expand Down Expand Up @@ -72,9 +72,9 @@
}

.drop-icon {
width: 64px;
height: 64px;
margin: 0 auto 12px;
width: 56px;
height: 56px;
margin: 0 auto 8px;
color: rgba(0, 255, 65, 0.5);
transition: all 0.4s ease;
}
Expand Down
40 changes: 36 additions & 4 deletions src/renderer/styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,18 @@ body {
color: #ff4444;
}

/* App Container */
/* App Container
NOTE: global UI scale is applied via webContents.setZoomFactor in main.js
(see APP_ZOOM_FACTOR). Layout paddings/gaps here are already tightened
to give comfortable spacing at that zoom, but NOT zoom-dependent. */
.app-container {
height: calc(100vh - 32px);
overflow-y: auto;
overflow-x: hidden;
padding: 16px 24px;
padding: 12px 20px;
display: flex;
flex-direction: column;
gap: 16px;
gap: 12px;
background:
radial-gradient(ellipse at 20% 50%, rgba(138, 43, 226, 0.08) 0%, transparent 50%),
radial-gradient(ellipse at 80% 20%, rgba(0, 255, 65, 0.04) 0%, transparent 50%),
Expand Down Expand Up @@ -154,7 +157,7 @@ body {
/* Header */
.app-header {
text-align: center;
padding: 8px 0;
padding: 4px 0;
}

.logo-container {
Expand Down Expand Up @@ -605,6 +608,35 @@ body {
display: none !important;
}

/* ---------- Already-Tagged Warning Banner ---------- */
.already-tagged-banner {
display: flex;
align-items: center;
gap: 10px;
margin: 0 0 12px 0;
padding: 10px 14px;
border: 1px solid #ff9100;
background: linear-gradient(90deg, rgba(255, 145, 0, 0.12), rgba(255, 23, 68, 0.08));
box-shadow: 0 0 10px rgba(255, 145, 0, 0.25), inset 0 0 10px rgba(255, 145, 0, 0.08);
color: #ffb347;
font-family: 'Share Tech Mono', monospace;
font-size: 12px;
letter-spacing: 1px;
text-shadow: 0 0 4px rgba(255, 145, 0, 0.6);
animation: atb-pulse 2.2s ease-in-out infinite;
}

.already-tagged-banner .atb-icon {
font-size: 16px;
color: #ff9100;
text-shadow: 0 0 6px #ff9100;
}

@keyframes atb-pulse {
0%, 100% { box-shadow: 0 0 8px rgba(255, 145, 0, 0.2), inset 0 0 8px rgba(255, 145, 0, 0.06); }
50% { box-shadow: 0 0 14px rgba(255, 145, 0, 0.4), inset 0 0 12px rgba(255, 145, 0, 0.12); }
}

/* ---------- Warning Modal ---------- */
.modal-overlay {
position: fixed;
Expand Down
Loading