Files
meetletacbfree/classes/registration/crypto.php
T

176 lines
5.8 KiB
PHP

<?php
// This file is part of Moodle - http://moodle.org/
/**
* Ed25519 + canonical JSON helpers (compatible with @meetlet/crypto).
*
* @package mod_meetletacbfree
* @copyright 2026 MeetLet
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_meetletacbfree\registration;
defined('MOODLE_INTERNAL') || die();
/**
* Crypto primitives for MeetLet registration protocol.
*/
class crypto {
/** MeetLet platform public key (SPKI PEM) — verify runtime authorizations only. */
public const PLATFORM_PUBLIC_KEY_PEM = <<<'PEM'
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAYyZTlJQzdGHyZ2aAZ45kJIOx4Rq1Opyy6OPpgRFoEtU=
-----END PUBLIC KEY-----
PEM;
/**
* Canonical JSON: sorted object keys, no whitespace.
*
* @param mixed $value
*/
public static function canonicalize($value): string {
return json_encode(self::sort_value($value), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
/**
* @param mixed $value
* @return mixed
*/
protected static function sort_value($value) {
if ($value instanceof \stdClass) {
$asarray = get_object_vars($value);
if ($asarray === []) {
return new \stdClass();
}
ksort($asarray);
$out = new \stdClass();
foreach ($asarray as $k => $v) {
$out->{$k} = self::sort_value($v);
}
return $out;
}
if (is_array($value)) {
if (self::is_list($value)) {
return array_map([self::class, 'sort_value'], $value);
}
ksort($value);
$out = [];
foreach ($value as $k => $v) {
$out[$k] = self::sort_value($v);
}
return $out;
}
return $value;
}
/**
* @param array $arr
*/
protected static function is_list(array $arr): bool {
if ($arr === []) {
return true;
}
return array_keys($arr) === range(0, count($arr) - 1);
}
public static function base64url_encode(string $bin): string {
return rtrim(strtr(base64_encode($bin), '+/', '-_'), '=');
}
public static function base64url_decode(string $b64): string {
$pad = 4 - (strlen($b64) % 4);
if ($pad < 4) {
$b64 .= str_repeat('=', $pad);
}
$raw = base64_decode(strtr($b64, '-_', '+/'), true);
if ($raw === false) {
throw new \moodle_exception('registration_crypto_error', 'meetletacbfree');
}
return $raw;
}
/**
* @return array{private_pem:string,public_pem:string,secret:string,public:string}
*/
public static function generate_installation_keypair(): array {
if (!function_exists('sodium_crypto_sign_keypair')) {
throw new \moodle_exception('registration_sodium_required', 'meetletacbfree');
}
$kp = sodium_crypto_sign_keypair();
$secret = sodium_crypto_sign_secretkey($kp);
$public = sodium_crypto_sign_publickey($kp);
$seed = substr($secret, 0, 32);
return [
'private_pem' => self::seed_to_pkcs8_pem($seed),
'public_pem' => self::raw_public_to_spki_pem($public),
'secret' => $secret,
'public' => $public,
];
}
public static function seed_to_pkcs8_pem(string $seed32): string {
$der = hex2bin('302e020100300506032b657004220420') . $seed32;
return "-----BEGIN PRIVATE KEY-----\n" .
chunk_split(base64_encode($der), 64, "\n") .
"-----END PRIVATE KEY-----\n";
}
public static function raw_public_to_spki_pem(string $public32): string {
$der = hex2bin('302a300506032b6570032100') . $public32;
return "-----BEGIN PUBLIC KEY-----\n" .
chunk_split(base64_encode($der), 64, "\n") .
"-----END PUBLIC KEY-----\n";
}
public static function spki_pem_to_raw(string $pem): string {
$pem = trim($pem);
$b64 = preg_replace('/-----[^-]+-----/', '', $pem);
$b64 = preg_replace('/\s+/', '', $b64);
$der = base64_decode($b64, true);
if ($der === false || strlen($der) < 32) {
throw new \moodle_exception('registration_crypto_error', 'meetletacbfree');
}
return substr($der, -32);
}
public static function pkcs8_pem_to_secret(string $pem): string {
$pem = trim($pem);
$b64 = preg_replace('/-----[^-]+-----/', '', $pem);
$b64 = preg_replace('/\s+/', '', $b64);
$der = base64_decode($b64, true);
if ($der === false || strlen($der) < 48) {
throw new \moodle_exception('registration_crypto_error', 'meetletacbfree');
}
$seed = substr($der, -32);
$kp = sodium_crypto_sign_seed_keypair($seed);
return sodium_crypto_sign_secretkey($kp);
}
public static function sign(string $privatepem, $payload): string {
$secret = self::pkcs8_pem_to_secret($privatepem);
$msg = self::canonicalize($payload);
$sig = sodium_crypto_sign_detached($msg, $secret);
return self::base64url_encode($sig);
}
public static function verify(string $publicpem, $payload, string $signatureb64url): bool {
try {
$public = self::spki_pem_to_raw($publicpem);
$msg = self::canonicalize($payload);
$sig = self::base64url_decode($signatureb64url);
return sodium_crypto_sign_verify_detached($sig, $msg, $public);
} catch (\Throwable $e) {
return false;
}
}
public static function random_nonce(): string {
return self::base64url_encode(random_bytes(16));
}
public static function random_installation_id(): string {
return 'mdl-' . bin2hex(random_bytes(16));
}
}