$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', 'meetletacbmarketplace'); } 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', 'meetletacbmarketplace'); } $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', 'meetletacbmarketplace'); } 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', 'meetletacbmarketplace'); } $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)); } }