/** * Create a unique ID (e.g. for permalinks) * * @param int $length * @return string */ function uniqueId(int $length = 24) : string { if ($length < 1) { return ''; } $n = (int) ceil($length * 0.75); $str = \random_bytes($n); return Binary::safeSubstr(Base64UrlSafe::encode($str), 0, $length); }
/** * @route notary */ public function index() { \Airship\json_response(['status' => 'OK', 'channel' => $this->channel, 'message' => '', 'public_key' => Base64UrlSafe::encode($this->pk->getRawKeyMaterial())]); }
/** * Create a random workspace directory * * @return string * @throws \Error */ protected function createWorkspace() : string { do { $dirname = Base64UrlSafe::encode(\random_bytes(18)); } while (\is_dir(AIRSHIP_LOCAL_CONFIG . DIRECTORY_SEPARATOR . $dirname)); if (!\mkdir(AIRSHIP_LOCAL_CONFIG . DIRECTORY_SEPARATOR . $dirname, 0700)) { throw new \Error('Could not create workspace directory: ' . AIRSHIP_LOCAL_CONFIG . DIRECTORY_SEPARATOR . $dirname); } return AIRSHIP_LOCAL_CONFIG . DIRECTORY_SEPARATOR . $dirname; }
/** * Get/verify/parse a JSON response * * The _server_ is the one that signs the message. * We're just verifying the Ed25519 signature. * * @param string $url * @param SignaturePublicKey $publicKey * @param array $args * @param array $options * @return array * @throws \Exception */ public static function postSignedJSON(string $url, SignaturePublicKey $publicKey, array $args = [], array $options = []) : array { $body = self::post($url, $args, $options); if (empty($body)) { throw new \Exception('Empty response from ' . $url); } if (self::$debug) { \var_dump($body); } $firstNewLine = \strpos($body, "\n"); // There should be a newline immediately after the base64urlsafe-encoded signature if ($firstNewLine !== self::ENCODED_SIGNATURE_LENGTH) { throw new \Exception('Invalid Signature'); } $sig = Base64UrlSafe::decode(Binary::safeSubstr($body, 0, self::ENCODED_SIGNATURE_LENGTH)); $msg = Binary::safeSubstr($body, self::ENCODED_SIGNATURE_LENGTH + 1); if (!Asymmetric::verify($msg, $publicKey, $sig, true)) { throw new \Exception('Invalid Signature'); } return \json_decode($msg, true); }
/** * Parse the HTTP response and get the useful information out of it. * * @param array $data * @param \DateTime $originated * @return array * @throws CouldNotUpdate */ protected function parseChannelUpdateResponse(array $data, \DateTime $originated) : array { if ($data['status'] !== 'success') { throw new CouldNotUpdate($data['message'] ?? \__('An update error has occurred')); } $valid = []; if (!empty($data['no_updates'])) { // Verify signature of the "no updates" timestamp. $sig = Base64UrlSafe::decode($data['signature']); if (!AsymmetricCrypto::verify($data['no_updates'], $this->channelPublicKey, $sig, true)) { throw new CouldNotUpdate(\__('Invalid signature from channel')); } $time = (new \DateTime($data['no_updates']))->add(new \DateInterval('P01D')); if ($time < $originated) { throw new CouldNotUpdate(\__('Channel is reporting a stale "no update" status')); } // No updates. return []; } // Verify the signature of each update. foreach ($data['updates'] as $update) { $data = Base64UrlSafe::decode($update['data']); $sig = Base64UrlSafe::decode($update['signature']); if (AsymmetricCrypto::verify($data, $this->channelPublicKey, $sig, true)) { $dataInternal = \json_decode($data, true); $valid[] = ['id' => (int) $update['id'], 'stored' => $dataInternal['stored'], 'master_signature' => $dataInternal['master_signature'], 'root' => $dataInternal['root'], 'data' => $dataInternal['data']]; } } // Sort by ID \uasort($valid, function (array $a, array $b) : int { return (int) ($a['id'] <=> $b['id']); }); return $valid; }
/** * Display the notary <meta> tag. * * @param SignaturePublicKey $pk */ function display_notary_tag(SignaturePublicKey $pk = null) { $state = State::instance(); $notary = $state->universal['notary']; if (!empty($notary['enabled'])) { if (!$pk) { $sk = $state->keyring['notary.online_signing_key']; if (!$sk instanceof SignatureSecretKey) { return; } $pk = $sk->derivePublicKey()->getRawKeyMaterial(); } echo '<meta name="airship-notary" content="' . Base64UrlSafe::encode($pk) . '; channel=' . Util::noHTML($notary['channel']) . '; url=' . cabin_url('Bridge') . 'notary' . '" />'; } }
/** * Parse a signed JSON response * * @param Response $response * @param SignaturePublicKey $publicKey * @return mixed * @throws SignatureFailed * @throws TransferException */ public function parseSignedJSON(Response $response, SignaturePublicKey $publicKey) { $code = $response->getStatusCode(); if ($code >= 200 && $code < 300) { $body = (string) $response->getBody(); $firstNewLine = \strpos($body, "\n"); // There should be a newline immediately after the base64urlsafe-encoded signature if ($firstNewLine !== self::ENCODED_SIGNATURE_LENGTH) { throw new SignatureFailed(\sprintf("First newline found at position %s, expected %d.\n%s", \print_r($firstNewLine, true), \print_r(self::ENCODED_SIGNATURE_LENGTH, true), Base64::encode($body))); } $sig = Base64UrlSafe::decode(Binary::safeSubstr($body, 0, 88)); $msg = Binary::safeSubstr($body, 89); if (!Asymmetric::verify($msg, $publicKey, $sig, true)) { throw new SignatureFailed(); } return \Airship\parseJSON($msg, true); } throw new TransferException(); }
/** * RFC 4648 Base64 (URL Safe) decoding * * "Zm9v" -> "foo" * * @param string $str * @return string */ public function base64UrlSafeDecode(string $str) : string { return Base64UrlSafe::decode($str, true); }
public function testEncoding() { $random_bytes = \random_bytes(31); // Backwards compatibility: $encoder = Halite::chooseEncoder(false); $this->assertSame(Hex::encode($random_bytes), $encoder($random_bytes)); $encoder = Halite::chooseEncoder(true); $this->assertSame(null, $encoder); // New encoding in version 3: $encoder = Halite::chooseEncoder(Halite::ENCODE_HEX); $this->assertSame(Hex::encode($random_bytes), $encoder($random_bytes)); $encoder = Halite::chooseEncoder(Halite::ENCODE_BASE32); $this->assertSame(Base32::encode($random_bytes), $encoder($random_bytes)); $encoder = Halite::chooseEncoder(Halite::ENCODE_BASE32HEX); $this->assertSame(Base32Hex::encode($random_bytes), $encoder($random_bytes)); $encoder = Halite::chooseEncoder(Halite::ENCODE_BASE64); $this->assertSame(Base64::encode($random_bytes), $encoder($random_bytes)); $encoder = Halite::chooseEncoder(Halite::ENCODE_BASE64URLSAFE); $this->assertSame(Base64UrlSafe::encode($random_bytes), $encoder($random_bytes)); }