@@ -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 ,
@@ -529,6 +576,118 @@ export async function injectSystemCertificate(
529576 }
530577}
531578
579+ // Read device's current CT log list. Undefined if there is none, but throws if it can't
580+ // be read for some reason.
581+ export async function readCtLogList (
582+ adbClient : Adb . DeviceClient ,
583+ runAsRoot : RootCmd
584+ ) : Promise < Buffer | undefined > {
585+ // Multiple possible formats, most recent is used by preference. Can't pull directly as we
586+ // need `su`, so we read on-device and base64 to avoid binary getting mangled in transfer.
587+ // To confirm the exact result, we need to wrap our output with extra info, otherwise we
588+ // can't reliably differentiate missing/error/truncated/OK.
589+ const output = await runRootScript ( adbClient , runAsRoot , 'htk-read-ct-logs.sh' , `
590+ for LIST_PATH in ${ [
591+ CT_LOG_LIST_PATHS . v3 ,
592+ CT_LOG_LIST_PATHS . v2 ,
593+ CT_LOG_LIST_PATHS . v1
594+ ] . join ( ' ' ) } ; do
595+ if [ -f "$LIST_PATH" ]; then
596+ echo "HTK-CT-LIST $LIST_PATH"
597+ base64 "$LIST_PATH"
598+ echo "HTK-CT-READ $?"
599+ exit 0
600+ fi
601+ done
602+
603+ echo "HTK-CT-LIST none"
604+ echo "HTK-CT-READ 0"
605+ ` , { timeout : 3000 , skipLogging : true } ) ;
606+
607+ 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 + ) / ) ;
608+ if ( ! result ) {
609+ throw new Error ( `Could not read the device's CT log list: ${
610+ output . trim ( ) . slice ( 0 , 200 ) || 'no output'
611+ } `) ;
612+ }
613+
614+ const [ , listPath , listContent , readResult ] = result ;
615+
616+ if ( listPath === 'none' ) {
617+ console . log ( 'No existing CT log list found on device' ) ;
618+ return undefined ;
619+ }
620+
621+ if ( readResult !== '0' ) {
622+ throw new Error ( `Reading ${ listPath } failed with status ${ readResult } ` ) ;
623+ }
624+
625+ console . log ( `Read existing CT log list from ${ listPath } ` ) ;
626+ return Buffer . from ( listContent . replace ( / [ ^ A - Z a - z 0 - 9 + / = ] / g, '' ) , 'base64' ) ;
627+ }
628+
629+ export async function injectCtLogLists (
630+ adbClient : Adb . DeviceClient ,
631+ runAsRoot : RootCmd ,
632+ logLists : { json : Buffer , ctfb : Buffer }
633+ ) {
634+ const jsonPath = `${ ANDROID_TEMP } /htk-ct-log-list.json` ;
635+ const ctfbPath = `${ ANDROID_TEMP } /htk-ct-log-list.ctfb` ;
636+
637+ // Use tmpfs to shadow device's CT config with our extended version:
638+ await runRootScript ( adbClient , runAsRoot , 'htk-inject-ct-logs.sh' , `
639+ set -e # Fail on error
640+
641+ echo "\n---\nInjecting CT log lists:"
642+
643+ # Drop any previous injection, so that repeated runs don't stack up mounts. This
644+ # has to be lazy: apps keep the log list mmapped, so a normal umount fails while
645+ # any of them are still running.
646+ for i in 1 2 3 4 5; do umount -l ${ CT_LOG_DIR } 2>/dev/null || break; done
647+
648+ # A fresh tmpfs is labelled u:object_r:tmpfs:s0, which apps can't read, so we
649+ # relabel everything to match the SELinux context Android uses here normally:
650+ set -- $(ls -Zd /data/misc/keychain 2>/dev/null || true)
651+ CT_CONTEXT=$1
652+ case "$CT_CONTEXT" in
653+ u:object_r:*) ;;
654+ *) CT_CONTEXT=u:object_r:keychain_data_file:s0 ;;
655+ esac
656+
657+ mkdir -p ${ CT_LOG_DIR }
658+ mount -t tmpfs tmpfs ${ CT_LOG_DIR }
659+
660+ mkdir -p ${ [
661+ CT_LOG_LIST_PATHS . v1 ,
662+ CT_LOG_LIST_PATHS . v2 ,
663+ CT_LOG_LIST_PATHS . v3
664+ ] . map ( ( listPath ) => path . posix . dirname ( listPath ) ) . join ( ' ' ) }
665+
666+ mv ${ jsonPath } ${ CT_LOG_LIST_PATHS . v1 }
667+ cp ${ CT_LOG_LIST_PATHS . v1 } ${ CT_LOG_LIST_PATHS . v2 }
668+ mv ${ ctfbPath } ${ CT_LOG_LIST_PATHS . v3 }
669+
670+ # Make everything as readable as the real log lists are:
671+ chown -R root:root ${ CT_LOG_DIR }
672+ chmod -R 755 ${ CT_LOG_DIR }
673+ chmod 644 ${ Object . values ( CT_LOG_LIST_PATHS ) . join ( ' ' ) }
674+ chcon -R $CT_CONTEXT ${ CT_LOG_DIR }
675+
676+ # Read-only, so that the daily CT log list update can't replace our lists. The
677+ # update fails & is retried later instead, which is harmless.
678+ mount -o remount,ro tmpfs ${ CT_LOG_DIR } ||
679+ echo 'Could not remount the CT log lists as read-only'
680+
681+ echo "CT log lists successfully injected\n---\n"
682+ ` , {
683+ files : [
684+ { content : logLists . json , path : jsonPath } ,
685+ { content : logLists . ctfb , path : ctfbPath }
686+ ] ,
687+ successMarker : 'CT log lists successfully injected'
688+ } ) ;
689+ }
690+
532691export async function setChromeFlags (
533692 adbClient : Adb . DeviceClient ,
534693 runAsRoot : RootCmd ,
0 commit comments