PHP Classes

File: src/Operations/Wrap/Pie.php

Recommend this page to a friend!
  Classes of Scott Arciszewski   PASERK PHP   src/Operations/Wrap/Pie.php   Download  
File: src/Operations/Wrap/Pie.php
Role: Class source
Content type: text/plain
Description: Class source
Class: PASERK PHP
Extend PASETO to wrap and serialize keys
Author: By
Last change:
Date: 1 year ago
Size: 13,035 bytes
 

Contents

Class file image Download
<?php declare(strict_types=1); namespace ParagonIE\Paserk\Operations\Wrap; use ParagonIE\ConstantTime\{ Base64UrlSafe, Binary }; use ParagonIE\Paseto\KeyInterface; use ParagonIE\Paseto\Keys\{ AsymmetricSecretKey, SymmetricKey }; use ParagonIE\Paseto\ProtocolInterface; use ParagonIE\Paseto\Protocol\{ Version3, Version4 }; use ParagonIE\Paserk\{ Operations\WrapInterface, PaserkException, Util }; use Exception; use SodiumException; use function array_slice, chunk_split, explode, hash_equals, hash_hmac, implode, in_array, openssl_decrypt, openssl_encrypt, random_bytes; /** * Class Pie * @package ParagonIE\Paserk\Operations\Wrap * * @link https://github.com/paseto-standard/paserk/blob/master/operations/Wrap/pie.md */ class Pie implements WrapInterface { const DOMAIN_SEPARATION_ENCRYPT = "\x80"; const DOMAIN_SEPARATION_AUTH = "\x81"; protected SymmetricKey $wrappingKey; /** * Pie constructor. * @param SymmetricKey $wrappingKey */ public function __construct(SymmetricKey $wrappingKey) { $this->wrappingKey = $wrappingKey; } /** * @return string */ public static function customId(): string { return 'pie'; } /** * @return ProtocolInterface */ public function getProtocol(): ProtocolInterface { return $this->wrappingKey->getProtocol(); } /** * @param string $header * @param KeyInterface $key * @return string * * @throws Exception * @throws PaserkException * @throws SodiumException */ public function wrapKey(string $header, KeyInterface $key): string { // Step 1: Algorithm Lucidity $this->throwIfVersionsMismatch($key->getProtocol()); $protocol = $key->getProtocol(); if ($protocol instanceof Version3) { return $this->wrapKeyV3($header, $key); } if ($protocol instanceof Version4) { return $this->wrapKeyV4($header, $key); } throw new PaserkException('Unknown key version'); } /** * Wrap a key according to the PIE spec. V1/V3 * * @ref https://github.com/paseto-standard/paserk/blob/master/operations/Wrap/pie.md#v1v3-encryption * * @param string $header * @param KeyInterface $key * @return string * @throws Exception */ protected function wrapKeyV3(string $header, KeyInterface $key): string { // Step 2: $n = random_bytes(32); // Step 3: $x = hash_hmac('sha384', self::DOMAIN_SEPARATION_ENCRYPT . $n, $this->wrappingKey->raw(), true); /// @SPEC DETAIL: ^ must be 0x80 $Ek = Binary::safeSubstr($x, 0, 32); $n2 = Binary::safeSubstr($x, 32, 16); // Step 4: $Ak = Binary::safeSubstr( hash_hmac('sha384', self::DOMAIN_SEPARATION_AUTH . $n, $this->wrappingKey->raw(), true), /// @SPEC DETAIL: ^ must be 0x81 0, 32 ); // Step 5: // This includes some PHP-specific behavior you may not need in other implementations: $rawKeyBytes = '' . $key->raw(); if ($key->getProtocol() instanceof Version3 && Binary::safeStrlen($rawKeyBytes) !== 48) { // Get the raw scalar, not a PEM-encoded key if ($key instanceof AsymmetricSecretKey) { $rawKeyBytes = Base64UrlSafe::decode($key->encode()); } } $c = openssl_encrypt( $rawKeyBytes, 'aes-256-ctr', $Ek, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $n2 ); /// @SPEC DETAIL: Must use (Ek, n2) // Step 6: $t = hash_hmac( 'sha384', $header . $n . $c, $Ak, true ); /// @SPEC DETAIL: Must cover h || c || t, in that order. // Wipe keys from memory after use: Util::wipe($rawKeyBytes); Util::wipe($Ek); Util::wipe($n2); Util::wipe($x); Util::wipe($Ak); // Step 7: return Base64UrlSafe::encodeUnpadded($t . $n . $c); /// @SPEC DETAIL: Must return t || n || c (in that order) } /** * Wrap a key according to the PIE spec. V2/V4 * * @ref https://github.com/paseto-standard/paserk/blob/master/operations/Wrap/pie.md#v2v4-encryption * * @param string $header * @param KeyInterface $key * @return string * * @throws Exception * @throws SodiumException */ protected function wrapKeyV4(string $header, KeyInterface $key): string { // Step 2: $n = random_bytes(32); // Step 3: $x = sodium_crypto_generichash(self::DOMAIN_SEPARATION_ENCRYPT . $n, $this->wrappingKey->raw(), 56); /// @SPEC DETAIL: ^ Must be 0x80 /// @SPEC DETAIL: Length MUST be 56 bytes $Ek = Binary::safeSubstr($x, 0, 32); $n2 = Binary::safeSubstr($x, 32, 24); // Step 4: $Ak = sodium_crypto_generichash(self::DOMAIN_SEPARATION_AUTH . $n, $this->wrappingKey->raw()); /// @SPEC DETAIL: ^ Must be 0x81 // Step 5: $c = sodium_crypto_stream_xchacha20_xor($key->raw(), $n2, $Ek); /// @SPEC DETAIL: Must use (Ek, n2) // Step 6: $t = sodium_crypto_generichash($header . $n . $c, $Ak); // Wipe keys from memory after use: Util::wipe($Ek); Util::wipe($n2); Util::wipe($x); Util::wipe($Ak); // Step 7: return Base64UrlSafe::encodeUnpadded($t . $n . $c); /// @SPEC DETAIL: Must return t || n || c (in that order) } /** * Unwrap a key. * * @param string $wrapped * @return KeyInterface * * @throws PaserkException * @throws Exception */ public function unwrapKey(string $wrapped): KeyInterface { // Step 1: Algorithm Lucidity // First, assert the version is correct. $pieces = explode('.', $wrapped); $version = Util::getPasetoVersion($pieces[0]); $this->throwIfVersionsMismatch($version); // Make sure this wasn't wrapped using a different custom key-wrapping protocol: if (!hash_equals($pieces[2], self::customId())) { throw new PaserkException('Key is not wrapped with the PIE key-wrapping protocol'); } $header = implode('.', array_slice($pieces, 0, 3)) . '.'; if ($pieces[0] === 'k3') { // We're in v1 or v3 mode. $bytes = $this->unwrapKeyV3($header, $pieces[3]); if ($pieces[1] === 'secret-wrap') { // Handle ECDSA private keys if (Binary::safeStrlen($bytes) !== 48) { throw new PaserkException("Unwrapped ECDSA secret key must be 48 bytes"); } // If we're here, we have a valid ECDSA secret key. } elseif (Binary::safeStrlen($bytes) !== 32) { throw new PaserkException("Unwrapped local keys must be 32 bytes"); } // If we're here, we have a valid RSA/ECDSA secret key or 256-bit symmetric key. } elseif ($pieces[0] === 'k4') { // We're in v2 or v4 mode. $bytes = $this->unwrapKeyV4($header, $pieces[3]); if ($pieces[1] === 'secret-wrap') { if (Binary::safeStrlen($bytes) !== 64) { throw new PaserkException("Unwrapped Ed25519 secret keys must be 64 bytes"); } // Ed25519 secret keys are encoded as (seed || pk), as per NaCl/libsodium. } elseif (Binary::safeStrlen($bytes) !== 32) { // This condition is only checked for local-wrap tokens throw new PaserkException("Unwrapped local keys must be 32 bytes"); } // If we're here, we have a valid Ed25519 secret key or 256-bit symmetric key. } else { throw new PaserkException('Unknown version: ' . $pieces[0]); } // Once we've decoded the bytes correctly, initialize the key object. if (hash_equals($pieces[1], 'local-wrap')) { return new SymmetricKey($bytes, $version); } if (hash_equals($pieces[1], 'secret-wrap')) { return new AsymmetricSecretKey($bytes, $version); } // Final step: Abort if unknown wrapping type. throw new PaserkException('Unknown wrapping type: ' . $pieces[1]); } /** * Unwrap a key according to the PIE spec. V1/V3 * * @ref https://github.com/paseto-standard/paserk/blob/master/operations/Wrap/pie.md#v1v3-decryption * * @param string $header * @param string $encoded * @return string * @throws PaserkException */ protected function unwrapKeyV3(string $header, string $encoded): string { // Step 1: $decoded = Base64UrlSafe::decode($encoded); $t = Binary::safeSubstr($decoded, 0, 48); /// @SPEC DETAIL: The first 48 bytes will be `t` $n = Binary::safeSubstr($decoded, 48, 32); /// @SPEC DETAIL: The next 32 bytes will be the nonce `n` $c = Binary::safeSubstr($decoded, 80); /// @SPEC DETAIL: The remaining bytes will be the wrapped key // Step 2: $Ak = Binary::safeSubstr( hash_hmac( 'sha384', self::DOMAIN_SEPARATION_AUTH . $n, $this->wrappingKey->raw(), true ), /// @SPEC DETAIL: Must be 0x81 0, 32 ); // Step 3: $t2 = hash_hmac( 'sha384', $header . $n . $c, $Ak, true ); // Step 4: if (!hash_equals($t2, $t)) { Util::wipe($t2); Util::wipe($Ak); throw new PaserkException('Invalid authentication tag'); } /// @SPEC DETAIL: Must be a constant-time comparison. // Step 5: $x = hash_hmac('sha384', self::DOMAIN_SEPARATION_ENCRYPT . $n, $this->wrappingKey->raw(), true); /// @SPEC DETAIL: ^ Must be 0x80 $Ek = Binary::safeSubstr($x, 0, 32); $n2 = Binary::safeSubstr($x, 32, 16); // Step 6: $ptk = openssl_decrypt( $c, 'aes-256-ctr', $Ek, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $n2 ); // Wipe keys from memory after use: Util::wipe($Ek); Util::wipe($n2); Util::wipe($x); Util::wipe($Ak); return $ptk; } /** * Unwrap a key according to the PIE spec. V2/V4 * * @ref https://github.com/paseto-standard/paserk/blob/master/operations/Wrap/pie.md#v2v4-decryption * * @param string $header * @param string $encoded * @return string * * @throws PaserkException * @throws SodiumException */ protected function unwrapKeyV4(string $header, string $encoded): string { // Step 1: $decoded = Base64UrlSafe::decode($encoded); $t = Binary::safeSubstr($decoded, 0, 32); /// @SPEC DETAIL: The first 32 bytes will be `t` $n = Binary::safeSubstr($decoded, 32, 32); /// @SPEC DETAIL: The next 32 bytes will be the nonce `n` $c = Binary::safeSubstr($decoded, 64); /// @SPEC DETAIL: The remaining bytes will be the wrapped key // Step 2: $Ak = sodium_crypto_generichash(self::DOMAIN_SEPARATION_AUTH . $n, $this->wrappingKey->raw()); /// @SPEC DETAIL: ^ Must be 0x81 // Step 3: $t2 = sodium_crypto_generichash($header . $n . $c, $Ak); // Step 4: if (!hash_equals($t2, $t)) { Util::wipe($t2); Util::wipe($Ak); throw new PaserkException('Invalid authentication tag'); } /// @SPEC DETAIL: Must be a constant-time comparison. // Step 5: $x = sodium_crypto_generichash(self::DOMAIN_SEPARATION_ENCRYPT . $n, $this->wrappingKey->raw(), 56); /// @SPEC DETAIL: ^ Must be 0x80 $Ek = Binary::safeSubstr($x, 0, 32); $n2 = Binary::safeSubstr($x, 32, 24); // Step 6: $ptk = sodium_crypto_stream_xchacha20_xor($c, $n2, $Ek); // Wipe keys from memory after use: Util::wipe($Ek); Util::wipe($n2); Util::wipe($x); Util::wipe($Ak); returN $ptk; } /** * @param ProtocolInterface $given * @throws PaserkException */ private function throwIfVersionsMismatch(ProtocolInterface $given): void { $expect = $this->wrappingKey->getProtocol(); if (!hash_equals($expect::header(), $given::header())) { throw new PaserkException('Invalid key version.'); } } }