array( array('install_id'=>'char(32)','namespace'=>'char(32)','request_id'=>'bigint unsigned', 'generation'=>'char(32)','operation_id'=>'char(64)','payload'=>'mediumtext', 'digest'=>'char(64)','body_hmac'=>'char(64)'), array('PRIMARY'=>array(0,array('install_id','namespace','request_id')), 'operation_id'=>array(0,array('operation_id'))))); } public function installNativeRemoteSchema(): array { try { $this->connect(1);$this->validateSchema(); if($this->rows('SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA=? AND TABLE_NAME=?',array($this->profile['database'],$this->prefix.'native_remote_operations_v4'))){throw new OG_Privacy_Case_Refusal('native_remote_schema_exists');} $this->sql('CREATE TABLE '.$this->table('native_remote_operations_v4').' (install_id CHAR(32) NOT NULL,namespace CHAR(32) NOT NULL,request_id BIGINT UNSIGNED NOT NULL,generation CHAR(32) NOT NULL,operation_id CHAR(64) NOT NULL,payload MEDIUMTEXT NOT NULL,digest CHAR(64) NOT NULL,body_hmac CHAR(64) NOT NULL,PRIMARY KEY(install_id,namespace,request_id),UNIQUE KEY operation_id(operation_id)) ENGINE=InnoDB DEFAULT CHARSET=ascii COLLATE=ascii_bin'); $this->validateTableSpecs(self::nativeRemoteSpecs()); return array('status'=>'native_remote_schema_installed','erasure_complete'=>false,'dispatch_enabled'=>false); }catch(Throwable $e){return $this->failure($e,'remote_install');}finally{$this->finish();} } private static function nativeRemoteMac(string $generation,string $payload,string $secret): string { return hash_hmac('sha256',"og-native-remote-operation-v4\n".$generation."\n".$payload,hex2bin($secret)); } private function nativeRemoteSecret(): string { // Caller already owns the lifecycle lock. Never returned to a caller or logger. $rows=$this->rows('SELECT secret FROM '.$this->table('control').' WHERE singleton=1'); if(count($rows)!==1 || !preg_match('/^[a-f0-9]{64}$/D',$rows[0]['secret'])){throw new OG_Privacy_Case_Refusal('native_remote_authority_invalid');} return $rows[0]['secret']; } private function nativeRemoteRow(array $binding,string $generation,string $secret): ?array { $args=array($binding['install'],$binding['namespace'],$binding['request_id']); $where=' WHERE install_id=? AND namespace=? AND request_id=? LIMIT 2'; $meta=$this->rows('SELECT OCTET_LENGTH(payload) AS size FROM '.$this->table('native_remote_operations_v4').$where,$args); if(!$meta){return null;} if(count($meta)!==1 || (int)$meta[0]['size']>OG_Native_Remote_Binding::MAX_BYTES){throw new OG_Privacy_Case_Refusal('native_remote_damaged');} $rows=$this->rows('SELECT generation,operation_id,payload,digest,body_hmac FROM '.$this->table('native_remote_operations_v4').$where,$args); try { if(count($rows)!==1){throw new RuntimeException('row');}$row=$rows[0]; if($row['generation']!==$generation || !hash_equals(self::nativeRemoteMac($generation,$row['payload'],$secret),$row['body_hmac'])){throw new RuntimeException('integrity');} $request=json_decode($row['payload'],true,12,JSON_THROW_ON_ERROR); if(!is_array($request) || OG_Native_Remote_Binding::encode($request)!==$row['payload'] || !hash_equals(OG_Native_Remote_Binding::digest($request),$row['digest']) || $request['install']!==$binding['install'] || $request['namespace']!==$binding['namespace'] || $request['request_id']!==(string)$binding['request_id'] || $request['requester_email_hash']!==$binding['email_hash'] || $request['cutoff_seconds']!==(string)$binding['cutoff_seconds'] || $request['operation_id']!==$row['operation_id']){throw new RuntimeException('binding');} return array('operation_id'=>$row['operation_id'],'payload'=>$row['payload'],'request_digest'=>$row['digest']); }catch(Throwable $e){throw new OG_Privacy_Case_Refusal('native_remote_damaged');} } /** Caller must authorize the native request. No caller-supplied operation, hashes or payload. */ public function prepareNativeRemoteOperation(array $context,$requestId): array { $phase='remote_admit'; try { $binding=$this->nativeScopeBinding($context,$requestId,array_merge(self::nativeRemoteSpecs(),self::nativePageSpecs(),self::nativeOrderFenceSpecs())); $secret=$this->nativeRemoteSecret(); $old=$this->nativeRemoteRow($binding,$context['generation'],$secret); if($old!==null){ $phase='remote_commit';$this->commit(); return array_merge(array('status'=>'native_remote_operation_saved','replayed'=>true,'erasure_complete'=>false,'dispatch_enabled'=>false),$old); } $phase='remote_scope'; $state=$this->nativePages($binding); if(!$state['observed_end'] || !$state['pages']){throw new OG_Privacy_Case_Refusal('native_remote_scan_incomplete');} $fences=$this->nativeOrderFenceRows($binding); if(array_values($fences)!==$state['pages']){throw new OG_Privacy_Case_Refusal('native_remote_fences_incomplete');} $orders=array(); foreach($state['pages'] as $page){foreach($page['orders'] as $order){ $orders[]=array('ref'=>$order['ref'],'order_key_hash'=>$order['order_key_hash']); }} usort($orders,static function($a,$b){return strlen($a['ref'])<=>strlen($b['ref']) ?: strcmp($a['ref'],$b['ref']);}); $request=array('version'=>OG_Native_Remote_Binding::VERSION,'install'=>$binding['install'], 'namespace'=>$binding['namespace'],'request_id'=>(string)$requestId,'operation_id'=>bin2hex(random_bytes(32)), 'cutoff_seconds'=>(string)$binding['cutoff_seconds'],'requester_email_hash'=>$binding['email_hash'], 'evidence_digest'=>$state['last_digest'],'orders'=>$orders); $payload=OG_Native_Remote_Binding::encode($request);$digest=OG_Native_Remote_Binding::digest($request); $phase='remote_insert'; if($this->sql('INSERT INTO '.$this->table('native_remote_operations_v4').' (install_id,namespace,request_id,generation,operation_id,payload,digest,body_hmac) VALUES (?,?,?,?,?,?,?,?)', array($binding['install'],$binding['namespace'],$requestId,$context['generation'],$request['operation_id'],$payload,$digest,self::nativeRemoteMac($context['generation'],$payload,$secret)))!==1){throw new OG_Privacy_Case_Refusal('native_remote_conflict');} $saved=$this->nativeRemoteRow($binding,$context['generation'],$secret); if($saved!==array('operation_id'=>$request['operation_id'],'payload'=>$payload,'request_digest'=>$digest)){throw new OG_Privacy_Case_Refusal('native_remote_damaged');} $phase='remote_commit';$this->commit(); return array_merge(array('status'=>'native_remote_operation_saved','replayed'=>false,'erasure_complete'=>false,'dispatch_enabled'=>false),$saved); }catch(Throwable $e){return array_merge($this->failure($e,$phase),array('erasure_complete'=>false,'dispatch_enabled'=>false));}finally{$this->finish();} } }