<?php

declare(strict_types=1);

namespace PhpWA\Crypto;

use InvalidArgumentException;
use PhpWA\Exception\CryptoException;

final class X25519
{
    public static function generate(?RandomSourceInterface $random = null): KeyPair
    {
        $random = $random ?? new SystemRandomSource();
        return self::fromPrivate(self::clamp($random->bytes(32)));
    }

    public static function fromPrivate(string $privateKey): KeyPair
    {
        self::requireKey($privateKey, 'private key');
        try {
            $publicKey = \ParagonIE_Sodium_Compat::crypto_scalarmult_base($privateKey);
        } catch (\Throwable $error) {
            throw new CryptoException('Unable to derive Curve25519 public key.', 0, $error);
        }
        self::requireKey($publicKey, 'derived public key');
        return new KeyPair($privateKey, $publicKey);
    }

    public static function sharedSecret(string $privateKey, string $peerPublicKey): string
    {
        self::requireKey($privateKey, 'private key');
        self::requireKey($peerPublicKey, 'peer public key');
        try {
            $secret = \ParagonIE_Sodium_Compat::crypto_scalarmult($privateKey, $peerPublicKey);
        } catch (\Throwable $error) {
            throw new CryptoException('Curve25519 key agreement failed.', 0, $error);
        }
        self::requireKey($secret, 'shared secret');
        if (hash_equals(str_repeat("\0", 32), $secret)) {
            throw new CryptoException('Curve25519 peer key produced an invalid all-zero secret.');
        }
        return $secret;
    }

    public static function clamp(string $seed): string
    {
        self::requireKey($seed, 'seed');
        $seed[0] = chr(ord($seed[0]) & 248);
        $seed[31] = chr((ord($seed[31]) & 127) | 64);
        return $seed;
    }

    private static function requireKey(string $key, string $name): void
    {
        if (strlen($key) !== 32) {
            throw new InvalidArgumentException('Curve25519 ' . $name . ' must contain exactly 32 bytes.');
        }
    }

    private function __construct()
    {
    }
}
