array( array('install_id'=>'char(32)','namespace'=>'char(32)','request_id'=>'bigint unsigned','operation_id'=>'char(64)','payload'=>'mediumtext','body_hmac'=>'char(64)'), array('PRIMARY'=>array(0,array('install_id','namespace','request_id','operation_id'))))); } public function installNativeCoreSchema(): 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_core_journal_v1'))){throw new OG_Privacy_Case_Refusal('core_schema_exists');} $this->sql('CREATE TABLE '.$this->table('native_core_journal_v1').' (install_id CHAR(32) NOT NULL,namespace CHAR(32) NOT NULL,request_id BIGINT UNSIGNED NOT NULL,operation_id CHAR(64) NOT NULL,payload MEDIUMTEXT NOT NULL,body_hmac CHAR(64) NOT NULL,PRIMARY KEY(install_id,namespace,request_id,operation_id)) ENGINE=InnoDB DEFAULT CHARSET=ascii COLLATE=ascii_bin'); $this->validateTableSpecs(self::nativeCoreSpecs()); return array('status'=>'core_schema_installed','erasure_complete'=>false); }catch(Throwable $e){return $this->failure($e,'core_install');}finally{$this->finish();} } private static function coreJournalMac(string $payload,string $secret): string { if(!preg_match('/^[a-f0-9]{64}$/D',$secret)){throw new OG_Privacy_Case_Refusal('core_external_secret_required');} return hash_hmac('sha256',"og-native-core-journal-v1\n".$payload,hex2bin($secret)); } private function coreJournalRow(array $args,array $identity,string $secret): ?array { $where=' WHERE install_id=? AND namespace=? AND request_id=? AND operation_id=? LIMIT 2'; $size=$this->rows('SELECT OCTET_LENGTH(payload) AS size FROM '.$this->table('native_core_journal_v1').$where,$args); if(!$size){return null;} if(count($size)!==1||(int)$size[0]['size']>32768){throw new OG_Privacy_Case_Refusal('core_journal_refetch_required');} $rows=$this->rows('SELECT payload,body_hmac FROM '.$this->table('native_core_journal_v1').$where,$args); if(count($rows)!==1||!hash_equals(self::coreJournalMac($rows[0]['payload'],$secret),$rows[0]['body_hmac'])){throw new OG_Privacy_Case_Refusal('core_journal_refetch_required');} try{$r=json_decode($rows[0]['payload'],true,16,JSON_THROW_ON_ERROR);}catch(Throwable $e){throw new OG_Privacy_Case_Refusal('core_journal_refetch_required');} if(!is_array($r)||array_keys($r)!==array('identity','attempt','nonce','lease_until','next_at','state','record','refresh_note')|| $r['identity']!==$identity||OG_Native_Remote_Binding::canonical($r)!==$rows[0]['payload']|| !is_int($r['attempt'])||$r['attempt']<1||$r['attempt']>6||!is_string($r['nonce'])||!preg_match('/^[a-f0-9]{64}$/D',$r['nonce'])|| !is_int($r['lease_until'])||$r['lease_until']<0||!is_int($r['next_at'])||$r['next_at']<1|| !in_array($r['state'],array('leased','waiting','attention','confirmed','configuration'),true)||!is_string($r['record'])||strlen($r['record'])>24576|| (($r['state']==='confirmed')!==($r['record']!==''))||!is_string($r['refresh_note'])||strlen($r['refresh_note'])>128){throw new OG_Privacy_Case_Refusal('core_journal_refetch_required');} return array('value'=>$r,'hmac'=>$rows[0]['body_hmac']); } public function nativeCoreJournal(array $context,$requestId,string $store,#[SensitiveParameter] string $journalSecret,string $event='read',string $nonce='',?string $proof=null,?int $httpStatus=null,array $keys=array(),array $history=array(),array $revoked=array(),string $environment='production'): array { $base=array('erasure_complete'=>false,'local_cleanup_authorized'=>false,'restart_authorized'=>false); try { if(!in_array($event,array('read','claim','settle'),true)||$store===''||strlen($store)>253|| !in_array($environment,array('production','review'),true)|| ($event!=='settle'&&($nonce!==''||$proof!==null||$httpStatus!==null))|| ($event==='settle'&&(!preg_match('/^[a-f0-9]{64}$/D',$nonce)||($proof!==null&&strlen($proof)>12288)))){throw new OG_Privacy_Case_Refusal('core_journal_input');} self::coreJournalMac('',$journalSecret); $binding=$this->nativeScopeBinding($context,$requestId,array_merge(self::nativeRemoteSpecs(),self::nativeCoreSpecs(),self::nativeAttentionSpecs(),self::attentionAuthoritySpecs())); $operationSecret=$this->nativeRemoteSecret(); if(hash_equals($operationSecret,$journalSecret)){throw new OG_Privacy_Case_Refusal('core_external_secret_required');} $operation=$this->nativeRemoteRow($binding,$context['generation'],$operationSecret); if($operation===null){throw new OG_Privacy_Case_Refusal('core_operation_missing');} $request=json_decode($operation['payload'],true,32,JSON_THROW_ON_ERROR); $identity=array('install'=>$binding['install'],'namespace'=>$binding['namespace'],'request_id'=>(string)$requestId, 'generation'=>$context['generation'],'store'=>$store,'operation_id'=>$operation['operation_id'],'request_digest'=>OG_Native_Remote_Binding::digest($request)); $args=array($binding['install'],$binding['namespace'],$requestId,$operation['operation_id']); $refresh=''; try{$old=$this->coreJournalRow($args,$identity,$journalSecret);} catch(OG_Privacy_Case_Refusal $e){ if($event!=='claim'||$e->getMessage()!=='core_journal_refetch_required'){throw $e;} $raw=$this->rows('SELECT LEFT(payload,32769) AS payload,body_hmac FROM '.$this->table('native_core_journal_v1').' WHERE install_id=? AND namespace=? AND request_id=? AND operation_id=? LIMIT 2',$args); if(count($raw)!==1){throw $e;} $untrusted=json_decode($raw[0]['payload'],true); // Even after re-keying, a visible terminal state is never reset // automatically. Invalid storage cannot authorize an erasure. if(is_array($untrusted)&&($untrusted['state']??null)==='attention'){throw $e;} $old=array('hmac'=>$raw[0]['body_hmac'],'value'=>null); $refresh='refetch:'.hash('sha256',$raw[0]['payload']); } $r=$old['value']??null; if($r!==null&&$r['state']==='confirmed'){ $verified=OG_Native_Core_Record::read($request,$context['generation'],$store,$r['record'],$journalSecret,$keys,$history,$revoked,$environment); if($verified['status']==='server_committed_core'){$this->commit();return $base+array('status'=>'server_committed_core','automatic_retry'=>false);} if($event!=='claim'){throw new OG_Privacy_Case_Refusal('core_journal_refetch_required');} $refresh='reverify:'.hash('sha256',$r['record']);$r=null; } if($r!==null&&$r['state']==='configuration'&&$event!=='claim'){$this->commit();return $base+array('status'=>'core_store_domain_mismatch','automatic_retry'=>false);} if($event==='read'){$this->commit();return $base+array('status'=>$r===null?'core_unobserved':'core_'.$r['state'],'automatic_retry'=>false);} $clock=$this->rows('SELECT CAST(UNIX_TIMESTAMP() AS CHAR) AS now_seconds');$now=self::nativeAttemptInteger($clock[0]['now_seconds']??null); if($now>PHP_INT_MAX-86460){throw new OG_Privacy_Case_Refusal('core_clock');} if($event==='claim'){ if($r!==null&&$r['state']==='configuration'){ if($r['next_at']>$now+86400){throw new OG_Privacy_Case_Refusal('core_clock');} if($now<$r['next_at']){$this->commit();return $base+array('status'=>'core_store_domain_mismatch','automatic_retry'=>false);} // A signed configuration mismatch is not a terminal proof // failure. Permit a new diagnostic cycle at most daily. // Keep the CAS and historical alert; confer no authority. $refresh='configuration-recheck:'.hash('sha256',$old['hmac']);$r=null; } if($r!==null&&($r['state']==='attention'||$r['attempt']>=6)){ $this->nativeCoreRecordAttention($context,$requestId,$r['state']==='attention'?'core_proof_attention':'core_proof_exhausted',$now); $this->commit();return $base+array('status'=>$r['state']==='attention'?'core_attention':'core_budget_exhausted','automatic_retry'=>false); } if($r!==null&&($r['lease_until']>$now+60||$r['next_at']>$now+86400)){throw new OG_Privacy_Case_Refusal('core_clock');} if($r!==null&&max($r['lease_until'],$r['next_at'])>$now){$this->commit();return $base+array('status'=>'core_waiting','automatic_retry'=>false);} $attempt=($r['attempt']??0)+1; $r=array('identity'=>$identity,'attempt'=>$attempt,'nonce'=>bin2hex(random_bytes(32)), 'lease_until'=>$now+60,'next_at'=>$now+array(300,1800,7200,21600,86400,86400)[$attempt-1],'state'=>'leased','record'=>'','refresh_note'=>$refresh!==''?$refresh:($r['refresh_note']??'')); }else{ if($r===null||$r['state']!=='leased'||!hash_equals($r['nonce'],$nonce)||$r['lease_until']<=$now||$r['lease_until']>$now+60){throw new OG_Privacy_Case_Refusal('core_stale_lease');} $record=$httpStatus===200&&$proof!==null?OG_Native_Core_Record::seal($request,$context['generation'],$store,$nonce,$proof,$journalSecret,$keys,$history,$revoked,$environment):null; $r['lease_until']=0; if($record!==null){$r['record']=$record;$r['state']='confirmed';} else{ $mismatch=false; if($httpStatus===200&&$proof!==null){ // Diagnostic only. Never accept this proof for our store, // save it as confirmed, or adopt a response's domain. $candidate=json_decode($proof,true); $domain=is_array($candidate)?($candidate['store_domain']??null):null; if(is_string($domain)&&$domain!==$store){ $checked=OG_Native_Core_Proof::verify($request,$domain,$nonce,$proof,$keys,$history,$revoked,$environment); $mismatch=$checked['status']==='authenticated_remote_core'; } } $temporary=in_array($httpStatus,array(null,0,429),true)||($httpStatus>=500&&$httpStatus<=599&&$httpStatus!==501); $r['state']=$mismatch?'configuration':(($r['attempt']>=6||!$temporary)?'attention':'waiting'); if($mismatch){$r['next_at']=$now+86400;} } } $payload=OG_Native_Remote_Binding::canonical($r);$mac=self::coreJournalMac($payload,$journalSecret); if($old===null){$changed=$this->sql('INSERT INTO '.$this->table('native_core_journal_v1').' (install_id,namespace,request_id,operation_id,payload,body_hmac) VALUES (?,?,?,?,?,?)',array_merge($args,array($payload,$mac)));} else{$changed=$this->sql('UPDATE '.$this->table('native_core_journal_v1').' SET payload=?,body_hmac=? WHERE install_id=? AND namespace=? AND request_id=? AND operation_id=? AND body_hmac=?',array_merge(array($payload,$mac),$args,array($old['hmac'])));} if($changed!==1||($this->coreJournalRow($args,$identity,$journalSecret)['value']??null)!==$r){throw new OG_Privacy_Case_Refusal('core_journal_conflict');} if($r['state']==='attention'){$this->nativeCoreRecordAttention($context,$requestId,'core_proof_attention',$now);} if($r['state']==='configuration'){$this->nativeCoreRecordAttention($context,$requestId,'core_store_domain_mismatch',$now);} $this->commit(); $result=$base+array('status'=>$event==='claim'?'core_claimed':($r['state']==='confirmed'?'server_committed_core':($r['state']==='configuration'?'core_store_domain_mismatch':'core_'.$r['state'])),'automatic_retry'=>false); if($event==='claim'){$result['nonce']=$r['nonce'];$result['request']=$request;} return $result; }catch(Throwable $e){ if($e instanceof OG_Privacy_Case_Refusal&&in_array($e->getMessage(),array('core_stale_lease','core_clock','core_journal_refetch_required','core_external_secret_required','core_journal_input','core_operation_missing'),true)){ return $base+array('status'=>$e->getMessage(),'automatic_retry'=>false); } return $base+array_merge($this->failure($e,'core_journal'),array('automatic_retry'=>false)); } finally{$this->finish();} } }