mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 14:23:14 -06:00
Device-owner tier substrate + silent install, end-to-end QR provisioning (Android 12+ compliance, APK-derived checksum, URL pre-seed, zero-touch onboarding, guided a11y screen), content-expiry (#157) with player no-restart deferral, and the device-enhancement batch (#10/#12/#13/#14). Backward-compatible with 1.9.3 clients; all autonomous behaviors opt-in. QA + security review green. Closes #161, #157, #159.
69 lines
3.4 KiB
JavaScript
69 lines
3.4 KiB
JavaScript
'use strict';
|
|
|
|
// #device-owner: the DER walk that pulls the signing cert out of an APK's PKCS#7 v1 block,
|
|
// so the provisioning QR checksum is computed from the served build (not hardcoded). Driven by a
|
|
// synthetic PKCS#7 SignedData (no APK needed) that exercises both short- and long-form DER lengths
|
|
// (real certs are >127 bytes -> long form). The end-to-end value against the real release APK is
|
|
// verified at deploy time; this guards the parser against regressions.
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const crypto = require('node:crypto');
|
|
const { _firstCertFromPkcs7 } = require('../lib/apk-signature');
|
|
|
|
// Minimal DER TLV encoder (definite length, short + long form).
|
|
function der(tag, content) {
|
|
let len;
|
|
if (content.length < 0x80) {
|
|
len = Buffer.from([content.length]);
|
|
} else {
|
|
const bytes = [];
|
|
let n = content.length;
|
|
while (n > 0) { bytes.unshift(n & 0xff); n >>= 8; }
|
|
len = Buffer.from([0x80 | bytes.length, ...bytes]);
|
|
}
|
|
return Buffer.concat([Buffer.from([tag]), len, content]);
|
|
}
|
|
|
|
function buildPkcs7(certContent1, certContent2) {
|
|
const cert1 = der(0x30, certContent1); // first X.509 cert (SEQUENCE)
|
|
const cert2 = der(0x30, certContent2); // a second cert we must NOT pick
|
|
const certs = der(0xa0, Buffer.concat([cert1, cert2]));// [0] IMPLICIT certificates
|
|
const signedData = der(0x30, Buffer.concat([
|
|
der(0x02, Buffer.from([0x01])), // version
|
|
der(0x31, Buffer.alloc(0)), // digestAlgorithms SET (empty)
|
|
der(0x30, Buffer.alloc(0)), // encapContentInfo (empty)
|
|
certs,
|
|
]));
|
|
const oid = der(0x06, Buffer.from([0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02])); // signedData OID
|
|
const contentInfo = der(0x30, Buffer.concat([oid, der(0xa0, signedData)]));
|
|
return { pkcs7: contentInfo, cert1 };
|
|
}
|
|
|
|
test('firstCertFromPkcs7 extracts the FIRST cert, exact bytes (short-form lengths)', () => {
|
|
const { pkcs7, cert1 } = buildPkcs7(Buffer.from([0x02, 0x01, 0x2a]), Buffer.from([0x02, 0x01, 0x2b]));
|
|
const got = _firstCertFromPkcs7(pkcs7);
|
|
assert.ok(got, 'a cert was found');
|
|
assert.deepEqual(Buffer.from(got), cert1, 'returns the full DER of cert1, not cert2 or a slice');
|
|
});
|
|
|
|
test('firstCertFromPkcs7 handles long-form lengths (real certs are >127 bytes)', () => {
|
|
const big1 = Buffer.alloc(300, 0xaa); // forces long-form length at cert + every wrapper
|
|
const big2 = Buffer.alloc(280, 0xbb);
|
|
const { pkcs7, cert1 } = buildPkcs7(big1, big2);
|
|
const got = Buffer.from(_firstCertFromPkcs7(pkcs7));
|
|
assert.deepEqual(got, cert1);
|
|
// And the checksum is a stable base64url(no-pad) SHA-256 of exactly those cert bytes.
|
|
const expect = crypto.createHash('sha256').update(cert1).digest('base64')
|
|
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
const actual = crypto.createHash('sha256').update(got).digest('base64')
|
|
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
assert.equal(actual, expect);
|
|
assert.match(actual, /^[A-Za-z0-9_-]{43}$/, 'SHA-256 base64url is 43 chars, url-safe, unpadded');
|
|
});
|
|
|
|
test('returns null on garbage / non-PKCS7 input (caller falls back, never emits a wrong QR)', () => {
|
|
assert.equal(_firstCertFromPkcs7(Buffer.from([0x30, 0x00])), null); // empty SEQUENCE
|
|
assert.equal(_firstCertFromPkcs7(Buffer.from([0x04, 0x01, 0x00])), null); // not a SEQUENCE-of-signedData
|
|
});
|