PHP Classes

File: src/Symmetric/Crypto.php

Recommend this page to a friend!
  Packages of Scott Arciszewski   Halite   src/Symmetric/Crypto.php   Download  
File: src/Symmetric/Crypto.php
Role: Class source
Content type: text/plain
Description: Class source
Class: Halite
Perform cryptography operations with libsodium
Author: By
Last change: For version 2, let's use strict types!
Merge branch 'v2.0' of https://github.com/paragonie/halite into v2.0

Conflicts:
src/Symmetric/Crypto.php
Update Crypto.php
Date: 10 years ago
Size: 8,139 bytes
 

Contents

Class file image Download
<?php
declare(strict_types=1);
namespace ParagonIE\Halite\Symmetric;

use \ParagonIE\Halite\Alerts as CryptoException;
use \ParagonIE\Halite\{
    Contract\SymmetricKeyCryptoInterface,
    Config,
    Halite,
    Symmetric\Config as SymmetricConfig,
    Util as CryptoUtil
};

abstract class Crypto implements SymmetricKeyCryptoInterface
{
    /**
     * Authenticate a string
     * 
     * @param string $message
     * @param AuthenticationKey $secretKey
     * @param boolean $raw
     * @throws CryptoException\InvalidKey
     * @return string
     */
    public static function authenticate(
        string $message,
        AuthenticationKey $secretKey,
        bool $raw = false
    ): string {
        $config = SymmetricConfig::getConfig(Halite::HALITE_VERSION, 'auth');
        $mac = self::calculateMAC($message, $secretKey->get(), $config);
        if ($raw) {
            return $mac;
        }
        return \Sodium\bin2hex($mac);
    }
    
    /**
     * Decrypt a message using the Halite encryption protocol
     * 
     * @param string $ciphertext
     * @param EncryptionKey $secretKey
     * @param boolean $raw Don't hex decode the input?
     * @return string
     * @throws CryptoException\InvalidMessage
     */
    public static function decrypt(
        string $ciphertext,
        EncryptionKey $secretKey,
        bool $raw = false
    ): string {
        if (!$raw) {
            // We were given hex data:
            $ciphertext = \Sodium\hex2bin($ciphertext);
        }
        list($version, $config, $salt, $nonce, $xored, $auth) = 
            self::unpackMessageForDecryption($ciphertext);
        
        // Split our keys
        list($eKey, $aKey) = self::splitKeys($secretKey, $salt, $config);
        
        // Check the MAC first
        if (!self::verifyMAC(
            $auth, 
            $version . $salt . $nonce . $xored,
            $aKey
        )) {
            throw new CryptoException\InvalidMessage(
                'Invalid message authentication code'
            );
        }
        
        // Down the road, do whatever logic around $version here, in case we
        // need to upgrade our protocol.
        
        // Add version logic above
        $plaintext = \Sodium\crypto_stream_xor($xored, $nonce, $eKey);
        if ($plaintext === false) {
            throw new CryptoException\InvalidMessage(
                'Invalid message authentication code'
            );
        }
        return $plaintext;
    }
    
    /**
     * Encrypt a message using the Halite encryption protocol
     * (Encrypt then MAC -- Xsalsa20 then HMAC-SHA-512/256)
     * 
     * @param string $plaintext
     * @param EncryptionKey $secretKey
     * @param boolean $raw Don't hex encode the output?
     * @return string
     */
    public static function encrypt(
        string $plaintext,
        EncryptionKey $secretKey,
        bool $raw = false
    ): string {
        $config = SymmetricConfig::getConfig(Halite::HALITE_VERSION, 'encrypt');
        
        // Generate a nonce and HKDF salt:
        $nonce = \Sodium\randombytes_buf(\Sodium\CRYPTO_SECRETBOX_NONCEBYTES);
        $salt = \Sodium\randombytes_buf($config->HKDF_SALT_LEN);
        
        // Split our keys according to the HKDF salt:
        list($eKey, $aKey) = self::splitKeys($secretKey, $salt, $config);
        
        // Encrypt our message with the encryption key:
        $xored = \Sodium\crypto_stream_xor($plaintext, $nonce, $eKey);
        
        // Calculate an authentication tag:
        $auth = self::calculateMAC(
            Halite::HALITE_VERSION . $salt . $nonce . $xored,
            $aKey
        );
        
        \Sodium\memzero($eKey);
        \Sodium\memzero($aKey);
        if (!$raw) {
            return \Sodium\bin2hex(
                Halite::HALITE_VERSION . $salt . $nonce . $xored . $auth
            );
        }
        return Halite::HALITE_VERSION . $salt . $nonce . $xored . $auth;
    }
    
    /**
     * Split a key using a variant of HKDF that used a keyed BLAKE2b hash rather
     * than an HMAC construct
     * 
     * @param EncryptionKey $master
     * @param string $salt
     * @param Config $config
     * @return array
     */
    public static function splitKeys(
        EncryptionKey $master,
        string $salt = '',
        Config $config = null
    ) {
        $binary = $master->get();
        return [
            CryptoUtil::hkdfBlake2b(
                $binary,
                \Sodium\CRYPTO_SECRETBOX_KEYBYTES,
                $config->HKDF_SBOX,
                $salt
            ),
            CryptoUtil::hkdfBlake2b(
                $binary,
                \Sodium\CRYPTO_AUTH_KEYBYTES,
                $config->HKDF_AUTH, 
                $salt
            )
        ];
    }
    
    /**
     * Unpack a message string into an array.
     * 
     * @param string $ciphertext
     * @return array
     */
    public static function unpackMessageForDecryption(string $ciphertext): array
    {
        $length = CryptoUtil::safeStrlen($ciphertext);
        
        // The first 4 bytes are reserved for the version size
        $version = CryptoUtil::safeSubstr($ciphertext, 0, Halite::VERSION_TAG_LEN);
        $config = SymmetricConfig::getConfig($version, 'encrypt');
        
        // The HKDF is used for key splitting
        $salt = CryptoUtil::safeSubstr(
            $ciphertext,
            Halite::VERSION_TAG_LEN,
            $config->HKDF_SALT_LEN
        );
        
        // This is the nonce (we authenticated it):
        $nonce = CryptoUtil::safeSubstr(
            $ciphertext, 
            // 36:
            Halite::VERSION_TAG_LEN + $config->HKDF_SALT_LEN,
            // 24:
            \Sodium\CRYPTO_STREAM_NONCEBYTES
        );
        
        // This is the crypto_stream_xor()ed ciphertext
        $xored = CryptoUtil::safeSubstr(
            $ciphertext, 
            // 60:
                Halite::VERSION_TAG_LEN +
                $config->HKDF_SALT_LEN +
                \Sodium\CRYPTO_STREAM_NONCEBYTES,
            // $length - 92:
            $length - (
                Halite::VERSION_TAG_LEN +
                $config->HKDF_SALT_LEN +
                \Sodium\CRYPTO_STREAM_NONCEBYTES +
                \Sodium\CRYPTO_AUTH_BYTES
            )
        );
        
        // $auth is the last 32 bytes
        $auth = CryptoUtil::safeSubstr($ciphertext, $length - \Sodium\CRYPTO_AUTH_BYTES);
        
        // We don't need this anymore.
        \Sodium\memzero($ciphertext);
        return [$version, $config, $salt, $nonce, $xored, $auth];
    }
    
    /**
     * Verify a MAC, given a MAC key
     * 
     * @param string $message
     * @param AuthenticationKey $secretKey
     * @param string $mac
     * @param boolean $raw
     * @return boolean
     */
    public static function verify(
        string $message,
        AuthenticationKey $secretKey,
        string $mac,
        bool $raw = false
    ): bool {
        if (!$raw) {
            $mac = \Sodium\hex2bin($mac);
        }
        return self::verifyMAC(
            $mac,
            $message,
            $secretKey->get()
        );
    }
    
    /**
     * Calculate a MAC
     * 
     * @param string $message
     * @param string $authKey
     * @return string
     */
    protected static function calculateMAC(
        string $message,
        string $authKey
    ): string {
        return \Sodium\crypto_auth(
            $message,
            $authKey
        );
    }
    
    /**
     * Verify a MAC
     * 
     * @param string $mac
     * @param string $message
     * @param string $aKey
     * @return bool
     */
    protected static function verifyMAC(
        string $mac,
        string $message,
        string $aKey
    ): bool {
        if (CryptoUtil::safeStrlen($mac) !== \Sodium\CRYPTO_AUTH_BYTES) {
            throw new CryptoException\InvalidSignature(
                'Message Authentication Code is not the correct length; is it encoded?'
            );
        }
        return \Sodium\crypto_auth_verify(
            $mac, 
            $message,
            $aKey
        );
    }
}