From e7f9f78f7caaa29a8621b33ca6ae7695aa4ba6af Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 30 Oct 2026 09:29:12 +0100 Subject: [PATCH] feat: add initial CryptX functional API bridge (#1220) Add a CryptX bootstrap and Java XS bridge for binary/base64url helpers, functional digests, and HMAC using JCA and Bouncy Castle. Cover the supported API slice against native CryptX and the bundled PerlOnJava module test. Generated with [OpenAI Codex](https://openai.com/codex/) Co-Authored-By: Codex --- dev/design/cryptx-port.md | 47 +++ docs/about/changelog.md | 2 + docs/reference/bundled-modules.md | 2 + docs/reference/feature-matrix.md | 3 + docs/reference/xs-compatibility.md | 1 + .../perlonjava/runtime/perlmodule/CryptX.java | 348 ++++++++++++++++++ src/main/perl/lib/CryptX.pm | 54 +++ .../module/CryptX/t/functional-api.t | 30 ++ 8 files changed, 487 insertions(+) create mode 100644 dev/design/cryptx-port.md create mode 100644 src/main/java/org/perlonjava/runtime/perlmodule/CryptX.java create mode 100644 src/main/perl/lib/CryptX.pm create mode 100644 src/test/resources/module/CryptX/t/functional-api.t diff --git a/dev/design/cryptx-port.md b/dev/design/cryptx-port.md new file mode 100644 index 0000000000..2f15f8ae2a --- /dev/null +++ b/dev/design/cryptx-port.md @@ -0,0 +1,47 @@ +# CryptX Java XS Port + +## Current state + +The first functional API slice is implemented on branch +`feature/cryptx-java-bridge`, based on master commit `d76b95e4093b24d9af782a69b2619a52d0b5ae6c`. +It retains a pure-Perl `CryptX.pm` bootstrap and implements selected XS entry +points in `CryptX.java` using the project's existing JCA and Bouncy Castle +dependencies. PR #1220 tracks the broader port. + +This slice covers `Crypt::Misc` binary/base64/base64url helpers, octet +increment, radix conversion, and equality; `Crypt::Digest` functional calls; +and `Crypt::Mac::HMAC` functional calls. Digest and HMAC algorithm support is +limited to names with a matching JCA or Bouncy Castle provider. Unsupported +names raise Perl runtime exceptions. Stateful digest/MAC objects, cipher and +mode families, public-key APIs, AEAD APIs, and remaining CryptX entry points +are follow-on work. + +## Decisions + +- Preserve the CPAN Perl wrappers and replace the XS boundary in Java; do not + duplicate the wrappers or translate LibTomCrypt's C implementation + mechanically. +- Reuse JCA and the already bundled Bouncy Castle provider. Keep provider + failures explicit and retain Perl-compatible `undef` behavior for malformed + base64 input. +- Add coverage first for functional APIs that directly support JWT-style + consumers. Validate focused tests against native CryptX before relying on + them under PerlOnJava. + +## Progress + +| Phase | Delivered scope | Evidence | Status | +|---|---|---|---| +| 1 | CryptX bootstrap; binary/base64url helpers; functional digests and HMAC | System Perl: 12/12 focused assertions. PerlOnJava: focused bundled-module test passed. `nice -n 19 make`: passed. | In review | + +## Next work + +Implement stateful digest and HMAC APIs, then add RSA signing/verification by +reusing `Crypt::OpenSSL::RSA` key parsing and signing paths. Expand to ECC/ECDH +and common AEAD APIs after the first downstream HS/RS JWT acceptance tests. + +## References + +- [CryptX port issue #1220](https://github.com/fglock/PerlOnJava/issues/1220) +- [XS compatibility inventory](../../docs/reference/xs-compatibility.md) +- [CPAN module porting guide](../../docs/guides/module-porting.md) diff --git a/docs/about/changelog.md b/docs/about/changelog.md index 3889964e81..fb03b8eb83 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -6,6 +6,8 @@ priorities and future plans. ## Work in progress +- Add an initial CryptX Java XS bridge for binary/base64url helpers, functional digests, and HMAC using the existing JCA and Bouncy Castle providers (#1220). + - Preserve named typeglob stringification when copying package stash entries (#1927). - Reduce repeated eval STRING overhead with lazy state and caching for context-free integer-literal evals (#1911). diff --git a/docs/reference/bundled-modules.md b/docs/reference/bundled-modules.md index c9d4f70dce..8feb3ad338 100644 --- a/docs/reference/bundled-modules.md +++ b/docs/reference/bundled-modules.md @@ -11,6 +11,7 @@ Recent CPAN compatibility additions include: | Module | Implementation | Notes | |--------|----------------|-------| +| `CryptX` | Perl bootstrap + Java XS bridge | Functional base64, digest, and HMAC entry points; broader CryptX APIs remain incomplete | | `Future::AsyncAwait` | Bundled pure Perl module plus PerlOnJava async runtime | Native `async`/`await` syntax and upstream-compatible Future integration | | `JSON::DWIW` | Pure Perl over bundled `JSON::PP` | Relaxed legacy JSON API, file conversion, booleans, and compatibility helpers | | `Taint::Runtime` | Perl + Java XS bridge | Runtime taint toggling and scalar taint inspection | @@ -305,6 +306,7 @@ These are loaded automatically or via `use`: | `Digest::MD5` | Java | `java.security.MessageDigest` | | `Digest::SHA` | Java | `java.security.MessageDigest` | | `Digest` | Perl | | +| `CryptX` | Perl + Java | Functional `Crypt::Misc` binary/base64url helpers, `Crypt::Digest` functional calls, and `Crypt::Mac::HMAC`; OO APIs and other crypto families are not yet covered | | `Crypt::Twofish2` | Java + Perl | Uses the bundled BouncyCastle provider; passes all 83 upstream vectors | | `Crypt::Rijndael` | Java + Perl | Uses the JCA AES provider; supports the upstream ECB, CBC, CFB128, OFB, and CTR API | | `MIME::Base64` | Java | | diff --git a/docs/reference/feature-matrix.md b/docs/reference/feature-matrix.md index 46f0b27a7f..952c223eca 100644 --- a/docs/reference/feature-matrix.md +++ b/docs/reference/feature-matrix.md @@ -910,6 +910,9 @@ The `:encoding()` layer supports all encodings provided by Java's `Charset.forNa upstream-compatible ECB, CBC, and CFB1 modes. - ✅ **Crypt::Rijndael**: Java XS replacement backed by the JCA AES provider, with upstream-compatible ECB, CBC, CFB128, OFB, and CTR modes. +- 🟡 **CryptX**: the upstream Perl bootstrap and a Java XS subset provide + base64 helpers, functional digests, and HMAC through JCA/Bouncy Castle; + stateful objects, ciphers, key APIs, and other XS families remain incomplete. - ✅ **Tie::Array::Packed**: Java XS replacement for packed tied-array storage, mutation, splicing, rotation, and binary search. - 🟡 **B::Flags**: portable OP/SV flag names over the bundled partial `B` diff --git a/docs/reference/xs-compatibility.md b/docs/reference/xs-compatibility.md index e17c9b3050..410d5a5e41 100644 --- a/docs/reference/xs-compatibility.md +++ b/docs/reference/xs-compatibility.md @@ -21,6 +21,7 @@ These modules have optimized Java implementations built into PerlOnJava: | DateTime | DateTime.java | 1.65 | Uses java.time APIs, JulianFields.RATA_DIE | | Digest::MD5 | DigestMD5.java | - | Uses Java MessageDigest | | Digest::SHA | DigestSHA.java | - | Uses Java MessageDigest | +| CryptX | CryptX.java | 0.091 | Functional binary/base64url helpers, selected digests, and HMAC; OO and other XS APIs remain incomplete | | Time::HiRes | TimeHiRes.java | - | Uses System.nanoTime() | | DBI | Dbi.java | - | JDBC backend | | Exporter::Lexical | ExporterLexical.java | 0.02 | Installs lexical subs into the enclosing compile-time scope | diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/CryptX.java b/src/main/java/org/perlonjava/runtime/perlmodule/CryptX.java new file mode 100644 index 0000000000..9cca846f2d --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/perlmodule/CryptX.java @@ -0,0 +1,348 @@ +package org.perlonjava.runtime.perlmodule; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.perlonjava.frontend.parser.StringParser; +import org.perlonjava.runtime.runtimetypes.*; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.Security; +import java.util.Base64; +import java.util.HexFormat; +import java.util.Locale; +import java.util.Map; + +import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarUndef; + +/** + * First Java-backed CryptX compatibility slice. CryptX's Perl wrappers remain + * upstream-owned; this class registers the common functional APIs used by + * JWT-style consumers without loading the distribution's XS library. + */ +public final class CryptX extends PerlModuleBase { + public static final String XS_VERSION = "0.091"; + private static final String MODULE = "CryptX"; + private static final Map DIGESTS = Map.ofEntries( + Map.entry("MD2", "MD2"), Map.entry("MD4", "MD4"), + Map.entry("MD5", "MD5"), Map.entry("SHA1", "SHA-1"), + Map.entry("SHA224", "SHA-224"), Map.entry("SHA256", "SHA-256"), + Map.entry("SHA384", "SHA-384"), Map.entry("SHA512", "SHA-512"), + Map.entry("SHA512224", "SHA-512/224"), Map.entry("SHA512256", "SHA-512/256"), + Map.entry("SHA3224", "SHA3-224"), Map.entry("SHA3256", "SHA3-256"), + Map.entry("SHA3384", "SHA3-384"), Map.entry("SHA3512", "SHA3-512"), + Map.entry("KECCAK224", "Keccak-224"), Map.entry("KECCAK256", "Keccak-256"), + Map.entry("KECCAK384", "Keccak-384"), Map.entry("KECCAK512", "Keccak-512"), + Map.entry("RIPEMD160", "RIPEMD160"), Map.entry("RIPEMD256", "RIPEMD256"), + Map.entry("RIPEMD320", "RIPEMD320"), Map.entry("BLAKE2B160", "BLAKE2B-160"), + Map.entry("BLAKE2B256", "BLAKE2B-256"), Map.entry("BLAKE2B384", "BLAKE2B-384"), + Map.entry("BLAKE2B512", "BLAKE2B-512"), Map.entry("BLAKE2S128", "BLAKE2S-128"), + Map.entry("BLAKE2S160", "BLAKE2S-160"), Map.entry("BLAKE2S224", "BLAKE2S-224"), + Map.entry("BLAKE2S256", "BLAKE2S-256") + ); + + public CryptX() { + this(MODULE); + } + + private CryptX(String perlPackage) { + super(perlPackage, false); + } + + static { + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + public static void initialize() { + CryptX core = new CryptX(MODULE); + CryptX misc = new CryptX("Crypt::Misc"); + CryptX digest = new CryptX("Crypt::Digest"); + CryptX hmac = new CryptX("Crypt::Mac::HMAC"); + try { + core.registerMethod("_ltc_build_settings", "buildSettings", null); + core.registerMethod("_ltc_mp_name", "mpName", null); + core.registerMethod("_ltc_mp_bits_per_digit", "mpBitsPerDigit", null); + + misc.registerMethod("encode_b64", "encodeBase64", null); + misc.registerMethod("encode_b64u", "encodeBase64Url", null); + misc.registerMethod("decode_b64", "decodeBase64", null); + misc.registerMethod("decode_b64u", "decodeBase64Url", null); + misc.registerMethod("slow_eq", "slowEquals", null); + misc.registerMethod("increment_octets_be", "incrementBigEndian", null); + misc.registerMethod("increment_octets_le", "incrementLittleEndian", null); + misc.registerMethod("_bin_to_radix", "binaryToRadix", null); + misc.registerMethod("_radix_to_bin", "radixToBinary", null); + + digest.registerMethod("digest_data", "digestData", null); + digest.registerMethod("digest_data_hex", "digestDataHex", null); + digest.registerMethod("digest_data_b64", "digestDataBase64", null); + digest.registerMethod("digest_data_b64u", "digestDataBase64Url", null); + + hmac.registerMethod("hmac", "hmac", null); + hmac.registerMethod("hmac_hex", "hmacHex", null); + hmac.registerMethod("hmac_b64", "hmacBase64", null); + hmac.registerMethod("hmac_b64u", "hmacBase64Url", null); + + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Unable to initialize " + MODULE, e); + } + GlobalVariable.getGlobalVariable("CryptX::VERSION").set(new RuntimeScalar(XS_VERSION)); + } + + public static RuntimeList buildSettings(RuntimeArray args, int ctx) { + return scalar("PerlOnJava JCA/Bouncy Castle providers"); + } + + public static RuntimeList mpName(RuntimeArray args, int ctx) { + return scalar("Java BigInteger"); + } + + public static RuntimeList mpBitsPerDigit(RuntimeArray args, int ctx) { + return scalar(32); + } + + public static RuntimeList encodeBase64(RuntimeArray args, int ctx) { + byte[] data = bytes(arg(args, 0), "Crypt::Misc::encode_b64"); + return scalar(Base64.getEncoder().encodeToString(data)); + } + + public static RuntimeList encodeBase64Url(RuntimeArray args, int ctx) { + byte[] data = bytes(arg(args, 0), "Crypt::Misc::encode_b64u"); + return scalar(Base64.getUrlEncoder().withoutPadding().encodeToString(data)); + } + + public static RuntimeList decodeBase64(RuntimeArray args, int ctx) { + return decodeBase64(args, false); + } + + public static RuntimeList decodeBase64Url(RuntimeArray args, int ctx) { + return decodeBase64(args, true); + } + + private static RuntimeList decodeBase64(RuntimeArray args, boolean url) { + String input = arg(args, 0).toString(); + String compact = input.replaceAll("\\s", ""); + try { + if (url) { + if (!compact.matches("[A-Za-z0-9_\\-=]*") || compact.indexOf('+') >= 0 || compact.indexOf('/') >= 0) { + return scalarUndef.getList(); + } + return new RuntimeScalar(Base64.getUrlDecoder().decode(compact)).getList(); + } + if (!compact.matches("[A-Za-z0-9+/=]*") || compact.indexOf('-') >= 0 || compact.indexOf('_') >= 0) { + return scalarUndef.getList(); + } + return new RuntimeScalar(Base64.getDecoder().decode(compact)).getList(); + } catch (IllegalArgumentException e) { + // CryptX's sane decoders return undef for malformed input. + return scalarUndef.getList(); + } + } + + public static RuntimeList slowEquals(RuntimeArray args, int ctx) { + byte[] left = bytes(arg(args, 0), "Crypt::Misc::slow_eq"); + byte[] right = bytes(arg(args, 1), "Crypt::Misc::slow_eq"); + return scalar(MessageDigest.isEqual(left, right) ? 1 : 0); + } + + public static RuntimeList incrementBigEndian(RuntimeArray args, int ctx) { + return increment(args, true); + } + + public static RuntimeList incrementLittleEndian(RuntimeArray args, int ctx) { + return increment(args, false); + } + + private static RuntimeList increment(RuntimeArray args, boolean bigEndian) { + byte[] data = bytes(arg(args, 0), "Crypt::Misc::increment_octets"); + byte[] result = data.clone(); + if (result.length == 0) return new RuntimeScalar(result).getList(); + if (bigEndian) { + for (int i = result.length - 1; i >= 0; i--) { + result[i]++; + if (result[i] != 0) return new RuntimeScalar(result).getList(); + } + } else { + for (int i = 0; i < result.length; i++) { + result[i]++; + if (result[i] != 0) return new RuntimeScalar(result).getList(); + } + } + throw new PerlCompilerException("FATAL: increment_octets overflow"); + } + + private static final String RADIX_DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/"; + + public static RuntimeList binaryToRadix(RuntimeArray args, int ctx) { + byte[] data = bytes(arg(args, 0), "Crypt::Misc::_bin_to_radix"); + int radix = arg(args, 1).getInt(); + if (radix < 2 || radix > 64) return scalarUndef.getList(); + if (data.length == 0) return scalar(""); + BigInteger value = new BigInteger(1, data); + if (value.signum() == 0) return scalar(""); + BigInteger base = BigInteger.valueOf(radix); + StringBuilder encoded = new StringBuilder(); + while (value.signum() != 0) { + BigInteger[] qr = value.divideAndRemainder(base); + encoded.append(RADIX_DIGITS.charAt(qr[1].intValue())); + value = qr[0]; + } + return scalar(encoded.reverse().toString()); + } + + public static RuntimeList radixToBinary(RuntimeArray args, int ctx) { + String input = arg(args, 0).toString(); + int radix = arg(args, 1).getInt(); + if (radix < 2 || radix > 64 || input.indexOf('\0') >= 0) return scalarUndef.getList(); + if (input.isEmpty()) return new RuntimeScalar(new byte[0]).getList(); + BigInteger value = BigInteger.ZERO; + for (int i = 0; i < input.length(); i++) { + int digit = RADIX_DIGITS.indexOf(input.charAt(i)); + if (digit < 0 || digit >= radix) return scalarUndef.getList(); + value = value.multiply(BigInteger.valueOf(radix)).add(BigInteger.valueOf(digit)); + } + if (value.signum() == 0) return new RuntimeScalar(new byte[0]).getList(); + byte[] signed = value.toByteArray(); + int offset = signed.length > 1 && signed[0] == 0 ? 1 : 0; + return new RuntimeScalar(java.util.Arrays.copyOfRange(signed, offset, signed.length)).getList(); + } + + public static RuntimeList digestData(RuntimeArray args, int ctx) { + return digest(args, 0); + } + + public static RuntimeList digestDataHex(RuntimeArray args, int ctx) { + return digest(args, 1); + } + + public static RuntimeList digestDataBase64(RuntimeArray args, int ctx) { + return digest(args, 2); + } + + public static RuntimeList digestDataBase64Url(RuntimeArray args, int ctx) { + return digest(args, 3); + } + + private static RuntimeList digest(RuntimeArray args, int output) { + String algorithm = arg(args, 0).toString(); + try { + MessageDigest digest = messageDigest(algorithm); + for (int i = 1; i < args.size(); i++) digest.update(bytes(args.get(i), "Crypt::Digest::digest_data")); + return encodeDigest(digest.digest(), output); + } catch (PerlCompilerException e) { + throw e; + } catch (Exception e) { + throw cryptoError("Crypt::Digest", algorithm, e); + } + } + + public static RuntimeList hmac(RuntimeArray args, int ctx) { + return calculateHmac(args, 0); + } + + public static RuntimeList hmacHex(RuntimeArray args, int ctx) { + return calculateHmac(args, 1); + } + + public static RuntimeList hmacBase64(RuntimeArray args, int ctx) { + return calculateHmac(args, 2); + } + + public static RuntimeList hmacBase64Url(RuntimeArray args, int ctx) { + return calculateHmac(args, 3); + } + + private static RuntimeList calculateHmac(RuntimeArray args, int output) { + String algorithm = arg(args, 0).toString(); + byte[] key = bytes(arg(args, 1), "Crypt::Mac::HMAC::hmac key"); + try { + Mac mac = mac(algorithm); + mac.init(new RawKey(key, mac.getAlgorithm())); + for (int i = 2; i < args.size(); i++) mac.update(bytes(args.get(i), "Crypt::Mac::HMAC::hmac data")); + return encodeDigest(mac.doFinal(), output); + } catch (PerlCompilerException e) { + throw e; + } catch (Exception e) { + throw cryptoError("Crypt::Mac::HMAC", algorithm, e); + } + } + + private static MessageDigest messageDigest(String algorithm) throws Exception { + String name = digestName(algorithm); + try { + return MessageDigest.getInstance(name); + } catch (Exception first) { + return MessageDigest.getInstance(name, "BC"); + } + } + + private static Mac mac(String algorithm) throws Exception { + String canonical = normalizeAlgorithm(algorithm); + String hash = DIGESTS.get(canonical); + if (hash == null) throw new PerlCompilerException("FATAL: Crypt::Mac::HMAC unsupported digest '" + algorithm + "'"); + String javaName = canonical.startsWith("SHA3") + ? "HMAC" + hash.replace("SHA3-", "SHA3-") + : "Hmac" + hash.replace("-", ""); + try { + return Mac.getInstance(javaName); + } catch (Exception first) { + return Mac.getInstance(javaName, "BC"); + } + } + + private static String digestName(String algorithm) { + String name = DIGESTS.get(normalizeAlgorithm(algorithm)); + if (name == null) throw new PerlCompilerException("FATAL: Crypt::Digest unsupported digest '" + algorithm + "'"); + return name; + } + + private static String normalizeAlgorithm(String algorithm) { + return algorithm.replace("_", "").replace("-", "").toUpperCase(Locale.ROOT); + } + + private static RuntimeList encodeDigest(byte[] data, int output) { + return switch (output) { + case 1 -> scalar(HexFormat.of().formatHex(data)); + case 2 -> scalar(Base64.getEncoder().encodeToString(data)); + case 3 -> scalar(Base64.getUrlEncoder().withoutPadding().encodeToString(data)); + default -> new RuntimeScalar(data).getList(); + }; + } + + private static PerlCompilerException cryptoError(String module, String algorithm, Exception cause) { + return new PerlCompilerException("FATAL: " + module + " failed for '" + algorithm + "': " + cause.getMessage()); + } + + private static RuntimeScalar arg(RuntimeArray args, int index) { + if (args.size() <= index) throw new PerlCompilerException("FATAL: CryptX missing argument"); + return args.get(index); + } + + private static byte[] bytes(RuntimeScalar value, String operation) { + String string = value.toString(); + StringParser.assertNoWideCharacters(string, operation); + return string.getBytes(StandardCharsets.ISO_8859_1); + } + + private static RuntimeList scalar(Object value) { + return new RuntimeScalar(value).getList(); + } + + private static final class RawKey implements SecretKey { + private final byte[] encoded; + private final String algorithm; + + private RawKey(byte[] encoded, String algorithm) { + this.encoded = encoded.clone(); + this.algorithm = algorithm; + } + + @Override public String getAlgorithm() { return algorithm; } + @Override public String getFormat() { return "RAW"; } + @Override public byte[] getEncoded() { return encoded.clone(); } + } +} diff --git a/src/main/perl/lib/CryptX.pm b/src/main/perl/lib/CryptX.pm new file mode 100644 index 0000000000..8d6dbe6dfb --- /dev/null +++ b/src/main/perl/lib/CryptX.pm @@ -0,0 +1,54 @@ +package CryptX; + +use strict; +use warnings; +our $VERSION = '0.091'; + +require XSLoader; +XSLoader::load(__PACKAGE__, $VERSION); + +use Carp; +my $has_json; + +BEGIN { + $has_json = 1 if eval { require JSON; 1 }; +} + +sub _croak { + die @_ if ref $_[0] || !$_[-1]; + if ($_[-1] =~ /^(.*)( at .+ line .+\n$)/s) { + pop @_; + push @_, $1; + } + die Carp::shortmess @_; +} + +sub _decode_json { + croak "FATAL: cannot find JSON module" if !$has_json; + return JSON->new->utf8->decode(shift); +} + +sub _encode_json { + croak "FATAL: cannot find JSON module" if !$has_json; + return JSON->new->utf8->canonical->encode(shift); +} + +1; + +__END__ + +=head1 NAME + +CryptX - Cryptographic toolkit bootstrap for PerlOnJava + +=head1 DESCRIPTION + +This file preserves the CryptX bootstrap and pure-Perl helpers. The supported +cryptographic entry points are provided by PerlOnJava's Java XSLoader bridge. + +=head1 COPYRIGHT AND LICENSE + +CryptX is Copyright (c) 2013-2026 DCIT, a.s. / Karel Miko. +This file is distributed under the same terms as Perl itself. + +=cut diff --git a/src/test/resources/module/CryptX/t/functional-api.t b/src/test/resources/module/CryptX/t/functional-api.t new file mode 100644 index 0000000000..0f99795ed4 --- /dev/null +++ b/src/test/resources/module/CryptX/t/functional-api.t @@ -0,0 +1,30 @@ +use strict; +use warnings; +use Test::More; +use CryptX; +use Crypt::PRNG qw(random_bytes); + +is(Crypt::Misc::encode_b64u("\x00\x01\x02\xff"), 'AAEC_w', 'base64url encodes binary data without padding'); +is(Crypt::Misc::decode_b64u('AAEC_w'), "\x00\x01\x02\xff", 'base64url decodes unpadded binary data'); +ok(!defined Crypt::Misc::decode_b64u('AA+_'), 'base64url rejects standard base64 alphabet'); + +is(Crypt::Digest::digest_data_hex('SHA256', 'abc'), + 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad', + 'SHA-256 functional digest matches the standard vector'); +is(Crypt::Digest::digest_data_b64u('SHA256', 'a', 'bc'), + 'ungWv48Bz-pBQUDeXa4iI7ADYaOWF3qctBD_YfIAFa0', + 'digest accepts multiple data chunks and returns base64url'); + +is(Crypt::Mac::HMAC::hmac_hex('SHA256', 'key', 'The quick brown fox jumps over the lazy dog'), + 'f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8', + 'HMAC-SHA-256 matches the standard vector'); +is(Crypt::Mac::HMAC::hmac_b64u('SHA256', 'key', 'message'), + 'bp7ym3X__Ft6uuUn1Y_a2y_kLnIZARl2kXNDBl9Y7Uo', + 'HMAC returns unpadded base64url'); +is(length(random_bytes(32)), 32, 'random_bytes returns the requested length'); +is(Crypt::Misc::increment_octets_be("\x01\xff"), "\x02\x00", 'big-endian octet increment'); +is(Crypt::Misc::increment_octets_le("\xff\x01"), "\x00\x02", 'little-endian octet increment'); +ok(Crypt::Misc::slow_eq('same', 'same'), 'slow_eq accepts equal values'); +ok(!Crypt::Misc::slow_eq('same', 'diff'), 'slow_eq rejects different values'); + +done_testing();