self::ABSENT, 'id' => 0, 'hash' => '', ); $stored = get_option( self::HEAD_OPTION, null ); if ( null === $stored ) { return $none; } if ( ! is_array( $stored ) || ! isset( $stored['id'], $stored['hash'], $stored['fp'], $stored['sig'] ) || ! is_scalar( $stored['id'] ) || ! Seal::is_digest( $stored['hash'] ) || ! is_string( $stored['fp'] ) || ! is_string( $stored['sig'] ) ) { return array_merge( $none, array( 'status' => self::FORGED ) ); } if ( ! Seal::same( Key::fingerprint( $key ), $stored['fp'] ) ) { return array_merge( $none, array( 'status' => self::KEY ) ); } $id = (int) $stored['id']; if ( ! Seal::same( Seal::sign( $key, 'head', $id . '|' . $stored['hash'] . '|' . $stored['fp'] ), $stored['sig'] ) ) { return array_merge( $none, array( 'status' => self::FORGED ) ); } return array( 'status' => self::OK, 'id' => $id, 'hash' => $stored['hash'], ); } /** * Record the newest sealed row. * * @param string $key Signing key. * @param int $id Row id. * @param string $hash Its chain hash. */ public static function write_head( string $key, int $id, string $hash ): void { $fp = Key::fingerprint( $key ); update_option( self::HEAD_OPTION, array( 'id' => $id, 'hash' => $hash, 'fp' => $fp, 'sig' => Seal::sign( $key, 'head', $id . '|' . $hash . '|' . $fp ), ), false ); } /** * The floor and the recorded cuts. * * @param string $key Signing key. * @return array{status: string, floor: int, cuts: array} */ public static function cuts( string $key ): array { $none = array( 'status' => self::ABSENT, 'floor' => 0, 'cuts' => array(), ); $stored = get_option( self::CUTS_OPTION, null ); if ( null === $stored ) { return $none; } if ( ! is_array( $stored ) || ! isset( $stored['floor'], $stored['cuts'], $stored['fp'], $stored['sig'] ) || ! is_scalar( $stored['floor'] ) || ! is_array( $stored['cuts'] ) || ! is_string( $stored['fp'] ) || ! is_string( $stored['sig'] ) ) { return array_merge( $none, array( 'status' => self::FORGED ) ); } if ( ! Seal::same( Key::fingerprint( $key ), $stored['fp'] ) ) { return array_merge( $none, array( 'status' => self::KEY ) ); } $cuts = array(); foreach ( $stored['cuts'] as $cut ) { if ( ! is_array( $cut ) || ! isset( $cut['from'], $cut['to'] ) || ! Seal::is_digest( $cut['from'] ) || ! Seal::is_digest( $cut['to'] ) ) { return array_merge( $none, array( 'status' => self::FORGED ) ); } $cuts[] = array( 'from' => $cut['from'], 'to' => $cut['to'], ); } $floor = max( 0, (int) $stored['floor'] ); if ( ! Seal::same( Seal::sign( $key, 'cuts', Cuts::canonical( $floor, $cuts ) . '|' . $stored['fp'] ), $stored['sig'] ) ) { return array_merge( $none, array( 'status' => self::FORGED ) ); } return array( 'status' => self::OK, 'floor' => $floor, 'cuts' => $cuts, ); } /** * Write the floor and cuts. * * @param string $key Signing key. * @param int $floor The floor. * @param array $cuts The cuts. */ public static function write_cuts( string $key, int $floor, array $cuts ): void { $fp = Key::fingerprint( $key ); update_option( self::CUTS_OPTION, array( 'floor' => $floor, 'cuts' => array_values( $cuts ), 'fp' => $fp, 'sig' => Seal::sign( $key, 'cuts', Cuts::canonical( $floor, $cuts ) . '|' . $fp ), ), false ); } /** * Add the runs a deletion is about to remove, keeping the floor. * * @param string $key Signing key. * @param array $runs The removed runs. */ public static function record( string $key, array $runs ): void { if ( array() === $runs ) { return; } $state = self::cuts( $key ); // A ledger that fails its signature, or was signed by another key, // is evidence: overwriting it with a fresh valid one would make the // next verification report nothing. The deletion that follows then // reads as a gap, which is true of a chain nobody can vouch for. if ( self::OK !== $state['status'] && self::ABSENT !== $state['status'] ) { return; } $cuts = $state['cuts']; foreach ( $runs as $run ) { $cuts = Cuts::add( $cuts, $run['from'], $run['to'] ); } self::write_cuts( $key, $state['floor'], $cuts ); } /** Remove both options. Uninstall. */ public static function uninstall(): void { delete_option( self::HEAD_OPTION ); delete_option( self::CUTS_OPTION ); } }