@@ -8,10 +8,16 @@ import { logError } from '../../error-tracking';
88import { waitUntil } from '../../util/promise' ;
99import { getCertificateFingerprint , parseCert } from '../../certificates' ;
1010import { streamToBuffer } from '../../util/stream' ;
11-
1211export const ANDROID_TEMP = '/data/local/tmp' ;
1312export const SYSTEM_CA_PATH = '/system/etc/security/cacerts' ;
1413
14+ const CT_LOG_DIR = '/data/misc/keychain/ct' ;
15+ const CT_LOG_LIST_PATHS = {
16+ v1 : `${ CT_LOG_DIR } /v1/current/log_list.json` ,
17+ v2 : `${ CT_LOG_DIR } /v2/current/log_list.json` ,
18+ v3 : `${ CT_LOG_DIR } /v3/current/log_list.ctfb` // Flatbuffer
19+ } ;
20+
1521export const EMULATOR_HOST_IPS = [
1622 '10.0.2.2' , // Standard emulator localhost ip
1723 '10.0.3.2' , // Genymotion localhost ip
@@ -189,11 +195,11 @@ const filterDeviceNameCache = (connectedIds: string[]) => {
189195} ;
190196
191197export function stringAsStream ( input : string ) {
192- const contentStream = new stream . Readable ( ) ;
193- contentStream . _read = ( ) => { } ;
194- contentStream . push ( input ) ;
195- contentStream . push ( null ) ;
196- return contentStream ;
198+ return bufferAsStream ( Buffer . from ( input , 'utf8' ) ) ;
199+ }
200+
201+ function bufferAsStream ( input : Buffer ) {
202+ return stream . Readable . from ( [ input ] , { objectMode : false } ) ;
197203}
198204
199205async function run (
@@ -279,7 +285,7 @@ const runAsRootCommands = [
279285 ( ...cmd : string [ ] ) => [ 'su' , 'root' , cmd . join ( ' ' ) ]
280286] ;
281287
282- type RootCmd = ( ...cmd : string [ ] ) => string [ ] ;
288+ export type RootCmd = ( ...cmd : string [ ] ) => string [ ] ;
283289
284290export async function getRootCommand ( adbClient : Adb . DeviceClient ) : Promise < RootCmd | undefined > {
285291 const rootTestScriptPath = `${ ANDROID_TEMP } /htk-root-test.sh` ;
@@ -412,6 +418,47 @@ const isMatchingCert = async (certStream: stream.Readable, expectedFingerprint:
412418 return expectedFingerprint === existingFingerprint ;
413419}
414420
421+ // Push a script, run it as root, and (optionally) check that it reported success. The
422+ // script deletes itself on any exit path, so nothing is left behind on the device.
423+ async function runRootScript (
424+ adbClient : Adb . DeviceClient ,
425+ runAsRoot : RootCmd ,
426+ scriptName : string ,
427+ script : string ,
428+ options : {
429+ files ?: Array < { content : Buffer , path : string } > ,
430+ successMarker ?: string ,
431+ timeout ?: number ,
432+ skipLogging ?: boolean
433+ } = { }
434+ ) {
435+ const scriptPath = `${ ANDROID_TEMP } /${ scriptName } ` ;
436+
437+ await Promise . all ( [
438+ ...( options . files ?? [ ] ) . map ( ( { content, path } ) =>
439+ // Due to an Android bug, user mode is always duplicated to group & others. We set as
440+ // read-only to avoid making these writable by others before we use them as root in a
441+ // moment. More details: https://github.com/openstf/adbkit/issues/126
442+ pushFile ( adbClient , bufferAsStream ( content ) , path , 0o444 )
443+ ) ,
444+ pushFile ( adbClient , stringAsStream ( `
445+ trap 'rm -f ${ scriptPath } ' EXIT
446+ ${ script }
447+ ` ) , scriptPath , 0o444 )
448+ ] ) ;
449+
450+ const output = await run ( adbClient , runAsRoot ( 'sh' , scriptPath ) , {
451+ timeout : options . timeout ?? 10000 ,
452+ skipLogging : options . skipLogging
453+ } ) ;
454+
455+ if ( options . successMarker && ! output . includes ( options . successMarker ) ) {
456+ throw new Error ( `${ scriptName } failed` ) ;
457+ }
458+
459+ return output ;
460+ }
461+
415462export async function injectSystemCertificate (
416463 adbClient : Adb . DeviceClient ,
417464 runAsRoot : RootCmd ,
@@ -526,7 +573,118 @@ export async function injectSystemCertificate(
526573
527574 if ( ! scriptOutput . includes ( "System cert successfully injected" ) ) {
528575 throw new Error ( 'System certificate injection failed' ) ;
576+ }
577+
578+ // Read device's current CT log list. Undefined if there is none, but throws if it can't
579+ // be read for some reason.
580+ export async function readCtLogList (
581+ adbClient : Adb . DeviceClient ,
582+ runAsRoot : RootCmd
583+ ) : Promise < Buffer | undefined > {
584+ // Multiple possible formats, most recent is used by preference. Can't pull directly as we
585+ // need `su`, so we read on-device and base64 to avoid binary getting mangled in transfer.
586+ // To confirm the exact result, we need to wrap our output with extra info, otherwise we
587+ // can't reliably differentiate missing/error/truncated/OK.
588+ const output = await runRootScript ( adbClient , runAsRoot , 'htk-read-ct-logs.sh' , `
589+ for LIST_PATH in ${ [
590+ CT_LOG_LIST_PATHS . v3 ,
591+ CT_LOG_LIST_PATHS . v2 ,
592+ CT_LOG_LIST_PATHS . v1
593+ ] . join ( ' ' ) } ; do
594+ if [ -f "$LIST_PATH" ]; then
595+ echo "HTK-CT-LIST $LIST_PATH"
596+ base64 "$LIST_PATH"
597+ echo "HTK-CT-READ $?"
598+ exit 0
599+ fi
600+ done
601+
602+ echo "HTK-CT-LIST none"
603+ echo "HTK-CT-READ 0"
604+ ` , { timeout : 3000 , skipLogging : true } ) ;
605+
606+ const result = output . match ( / H T K - C T - L I S T ( \S + ) \r ? \n ( [ \s \S ] * ) H T K - C T - R E A D ( \d + ) / ) ;
607+ if ( ! result ) {
608+ throw new Error ( `Could not read the device's CT log list: ${
609+ output . trim ( ) . slice ( 0 , 200 ) || 'no output'
610+ } `) ;
611+ }
612+
613+ const [ , listPath , listContent , readResult ] = result ;
614+
615+ if ( listPath === 'none' ) {
616+ console . log ( 'No existing CT log list found on device' ) ;
617+ return undefined ;
618+ }
619+
620+ if ( readResult !== '0' ) {
621+ throw new Error ( `Reading ${ listPath } failed with status ${ readResult } ` ) ;
529622 }
623+
624+ console . log ( `Read existing CT log list from ${ listPath } ` ) ;
625+ return Buffer . from ( listContent . replace ( / [ ^ A - Z a - z 0 - 9 + / = ] / g, '' ) , 'base64' ) ;
626+ }
627+
628+ export async function injectCtLogLists (
629+ adbClient : Adb . DeviceClient ,
630+ runAsRoot : RootCmd ,
631+ logLists : { json : Buffer , ctfb : Buffer }
632+ ) {
633+ const jsonPath = `${ ANDROID_TEMP } /htk-ct-log-list.json` ;
634+ const ctfbPath = `${ ANDROID_TEMP } /htk-ct-log-list.ctfb` ;
635+
636+ // Use tmpfs to shadow device's CT config with our extended version:
637+ await runRootScript ( adbClient , runAsRoot , 'htk-inject-ct-logs.sh' , `
638+ set -e # Fail on error
639+
640+ echo "\n---\nInjecting CT log lists:"
641+
642+ # Drop any previous injection, so that repeated runs don't stack up mounts. This
643+ # has to be lazy: apps keep the log list mmapped, so a normal umount fails while
644+ # any of them are still running.
645+ for i in 1 2 3 4 5; do umount -l ${ CT_LOG_DIR } 2>/dev/null || break; done
646+
647+ # A fresh tmpfs is labelled u:object_r:tmpfs:s0, which apps can't read, so we
648+ # relabel everything to match the SELinux context Android uses here normally:
649+ set -- $(ls -Zd /data/misc/keychain 2>/dev/null || true)
650+ CT_CONTEXT=$1
651+ case "$CT_CONTEXT" in
652+ u:object_r:*) ;;
653+ *) CT_CONTEXT=u:object_r:keychain_data_file:s0 ;;
654+ esac
655+
656+ mkdir -p ${ CT_LOG_DIR }
657+ mount -t tmpfs tmpfs ${ CT_LOG_DIR }
658+
659+ mkdir -p ${ [
660+ CT_LOG_LIST_PATHS . v1 ,
661+ CT_LOG_LIST_PATHS . v2 ,
662+ CT_LOG_LIST_PATHS . v3
663+ ] . map ( ( listPath ) => path . posix . dirname ( listPath ) ) . join ( ' ' ) }
664+
665+ mv ${ jsonPath } ${ CT_LOG_LIST_PATHS . v1 }
666+ cp ${ CT_LOG_LIST_PATHS . v1 } ${ CT_LOG_LIST_PATHS . v2 }
667+ mv ${ ctfbPath } ${ CT_LOG_LIST_PATHS . v3 }
668+
669+ # Make everything as readable as the real log lists are:
670+ chown -R root:root ${ CT_LOG_DIR }
671+ chmod -R 755 ${ CT_LOG_DIR }
672+ chmod 644 ${ Object . values ( CT_LOG_LIST_PATHS ) . join ( ' ' ) }
673+ chcon -R $CT_CONTEXT ${ CT_LOG_DIR }
674+
675+ # Read-only, so that the daily CT log list update can't replace our lists. The
676+ # update fails & is retried later instead, which is harmless.
677+ mount -o remount,ro tmpfs ${ CT_LOG_DIR } ||
678+ echo 'Could not remount the CT log lists as read-only'
679+
680+ echo "CT log lists successfully injected\n---\n"
681+ ` , {
682+ files : [
683+ { content : logLists . json , path : jsonPath } ,
684+ { content : logLists . ctfb , path : ctfbPath }
685+ ] ,
686+ successMarker : 'CT log lists successfully injected'
687+ } ) ;
530688}
531689
532690export async function setChromeFlags (
0 commit comments