mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Maxim Thomas
yesterday 3b20faae34218cdbb72008a3695f748b34cdff4a
opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc
@@ -844,7 +844,9 @@
====
OpenDJ ships the Bouncy Castle FIPS provider. The `setup` command and the server register it themselves when the server key store is a BCFKS key store (`--useBcfksKeystore`), and the server registers it at start whenever the `org.openidentityplatform.opendj.fips.register` Java system property is `true`, for example through the `OPENDJ_JAVA_ARGS` environment variable. With this provider in its default mode the default configuration works as it is. The approved-only mode of the provider (the `org.bouncycastle.fips.approved_only` Java system property), which OpenDJ does not turn on, is not covered here.
The crypto manager wraps the secret keys it shares with the other servers of a replication topology with each server's public key. The transformation it uses, the `key-wrapping-transformation` property of the crypto manager, is `RSA/ECB/OAEPWITHSHA-1ANDMGF1PADDING` by default: RSA-OAEP, the key transport scheme of NIST SP 800-56B, which Bouncy Castle FIPS provides. A Java runtime whose cryptography comes from a `SunPKCS11` provider alone, such as `SunPKCS11-NSS-FIPS` on a Linux system in FIPS mode, provides no RSA-OAEP at all: its only RSA cipher is `RSA/ECB/PKCS1Padding`, which NIST SP 800-131A Rev. 2 disallows for key transport. On such a runtime the server refuses to start with the default transformation, `setup` says so, and the choice of another one is yours to make: OpenDJ does not make it for you. If you make it, keep in mind that every server of a replication topology must use the same transformation, since each server unwraps what the others wrapped.
The crypto manager wraps the secret keys it shares with the other servers of a replication topology with each server's public key. The transformation it uses, the `key-wrapping-transformation` property of the crypto manager, is `RSA/ECB/OAEPWITHSHA-1ANDMGF1PADDING` by default: RSA-OAEP, the key transport scheme of NIST SP 800-56B, which Bouncy Castle FIPS provides. A Java runtime whose cryptography comes from a `SunPKCS11` provider alone, such as `SunPKCS11-NSS-FIPS` on a Linux system in FIPS mode, provides no RSA-OAEP at all: its RSA cipher offers PKCS#1 v1.5 padding, `RSA/ECB/PKCS1Padding`, which NIST SP 800-131A Rev. 2 disallows for key transport, and no padding, `RSA/ECB/NoPadding`. On such a runtime the server refuses to start with the default transformation, `setup` says so, and the choice of another one is yours to make: OpenDJ does not make it for you. If you make it, keep in mind that every server of a replication topology must use the same transformation, since each server unwraps what the others wrapped.
For such a runtime OpenDJ provides a key wrapping scheme of its own, `RSASVE/KDF-SHA256/AESKWP`, built on the hybrid key transport of NIST SP 800-56B Rev. 2, section 9.3. It encapsulates a random secret value with `RSA/ECB/NoPadding`, derives a key wrapping key from it with the one-step key derivation of NIST SP 800-56C Rev. 2 over SHA-256, and wraps the secret key with the AES key wrap with padding of NIST SP 800-38F (`AES/KWP/NoPadding`). No nonce of the receiving server enters the key derivation, because a server wraps keys for another server from its published certificate alone: the scheme is therefore not the KAS1-basic scheme of NIST SP 800-56B to the letter, and whether it is an approved key transport method for a FIPS 140-3 deployment is for the assessment of that deployment to decide. The runtime has to offer these three algorithms: when it lacks one, the server refuses the value and names the `key-wrapping-transformation` property. The `SunPKCS11` provider of OpenJDK 17, for example, offers no AES-KWP, so on an OpenJDK 17 runtime whose cryptography comes from that provider alone the server refuses the value at start; the provider offers AES-KWP from OpenJDK 19 on. A server which does not support the scheme cannot unwrap the secret keys wrapped with it, and neither can a server whose Java runtime lacks one of these algorithms: Java 11, for example, offers no AES-KWP unless the Bouncy Castle FIPS provider is registered. Such a server refuses nothing, since it does not use the scheme itself: it only logs an error for each key it cannot unwrap. So every server of the replication topology must run an OpenDJ version which supports the scheme, on a Java runtime which offers these three algorithms, before any server uses it. Setting the property back does not re-wrap the secret keys already wrapped with the scheme: every server of the replication topology must keep supporting it for as long as those keys are in use.
. Install the server without starting it:
+
@@ -871,7 +873,7 @@
dn: cn=Crypto Manager,cn=config
changetype: modify
replace: ds-cfg-key-wrapping-transformation
ds-cfg-key-wrapping-transformation: RSA/ECB/PKCS1Padding
ds-cfg-key-wrapping-transformation: RSASVE/KDF-SHA256/AESKWP
$ ldifmodify \
 --outputLDIF /path/to/opendj/config/config.ldif.new \
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/CryptoManagerConfiguration.xml
@@ -185,6 +185,24 @@
      The preferred key wrapping transformation for the directory server. This value must
      be the same for all server instances in a replication topology.
    </adm:synopsis>
    <adm:description>
      Besides the transformations of the Java runtime, the value
      RSASVE/KDF-SHA256/AESKWP selects a scheme built on the hybrid key
      transport of NIST SP 800-56B Rev. 2, section 9.3, without a nonce
      of the receiving server, for a Java runtime which offers no
      RSA-OAEP: it needs only RSA/ECB/NoPadding, SHA-256 and the AES key
      wrap with padding (AES/KWP/NoPadding) from the runtime. The
      SunPKCS11 provider of OpenJDK 17 offers no AES/KWP/NoPadding, so a
      server whose runtime has that provider alone refuses this value;
      the provider offers it from OpenJDK 19 on. A server which does not
      support this value cannot unwrap the keys wrapped with it, and
      neither can a server whose Java runtime lacks one of these
      algorithms, such as Java 11 without Bouncy Castle FIPS: it only
      logs an error for each key it cannot unwrap. So every server of
      the replication topology must run a version which supports this
      value, on a Java runtime which offers these algorithms, before any
      server uses it.
    </adm:description>
    <adm:requires-admin-action>
      <adm:none>
        <adm:synopsis>
opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerImpl.java
@@ -26,6 +26,7 @@
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
@@ -459,17 +460,25 @@
  }
  /**
   * Checks that this Java runtime provides the key wrapping transformation. Only a refusal here
   * names the key-wrapping-transformation property: the wrap which follows it also needs a MAC
   * key of the mac-algorithm property, and changing the key wrapping property does not help when
   * that is what the runtime refuses.
   * Checks that this Java runtime provides the key wrapping transformation, or every algorithm the
   * RSASVE scheme needs when the transformation names that scheme. Only a refusal here names the
   * key-wrapping-transformation property: the wrap which follows it also needs a MAC key of the
   * mac-algorithm property, and changing the key wrapping property does not help when that is what
   * the runtime refuses.
   */
  private static boolean isKeyWrappingTransformationSupported(
      final String transformation, final List<LocalizableMessage> unacceptableReasons)
  {
    try
    {
      Cipher.getInstance(transformation);
      if (RsaSveKeyTransport.isScheme(transformation))
      {
        RsaSveKeyTransport.checkSupported();
      }
      else
      {
        Cipher.getInstance(transformation);
      }
      return true;
    }
    catch (GeneralSecurityException ex)
@@ -847,6 +856,9 @@
   * wrappedKeyAlgorithm ::= e.g., DESede
   * hexifiedwrappedKey ::= 0123456789abcdef01...
   * </pre>
   * The wrapping transformation is a transformation of the Java runtime, or
   * RSASVE/KDF-SHA256/AESKWP, the scheme of {@link RsaSveKeyTransport}, whose
   * wrapped key is the one {@link RsaSveKeyTransport#wrap} returns.
   *
   * @param wrappingKeyID The key identifier of the wrapping key. This
   * parameter is the first field in the encoded value and identifies
@@ -915,10 +927,17 @@
              = CertificateFactory.getInstance("X.509");
      final Certificate certificate = cf.generateCertificate(
              new ByteArrayInputStream(wrappingKeyCertificateData));
      final Cipher wrapper
              = Cipher.getInstance(wrappingTransformationName);
      wrapper.init(Cipher.WRAP_MODE, certificate);
      byte[] wrappedKey = wrapper.wrap(secretKey);
      final byte[] wrappedKey;
      if (RsaSveKeyTransport.isScheme(wrappingTransformationName)) {
        wrappedKey = RsaSveKeyTransport.wrap(certificate,
                secretKey, wrappingKeyID, secureRandom);
      }
      else {
        final Cipher wrapper
                = Cipher.getInstance(wrappingTransformationName);
        wrapper.init(Cipher.WRAP_MODE, certificate);
        wrappedKey = wrapper.wrap(secretKey);
      }
      wrappedKeyElement = StaticUtils.bytesToHexNoSpace(wrappedKey);
    }
    catch (GeneralSecurityException ex) {
@@ -938,6 +957,8 @@
   * Takes an encoded ds-cfg-symmetric-key attribute value and the
   * associated key algorithm name, and returns an initialized
   * {@code java.security.Key} object.
   * <p>
   * Package private for testing.
   * @param symmetricKeyAttribute The encoded
   * ds-cfg-symmetric-key-attribute value.
   * @return A SecretKey object instantiated with the key data,
@@ -946,7 +967,7 @@
   * @throws CryptoManagerException If there is a problem decomposing
   * the supplied attribute value or unwrapping the encoded key.
   */
  private SecretKey decodeSymmetricKeyAttribute(
  SecretKey decodeSymmetricKeyAttribute(
          final String symmetricKeyAttribute)
          throws CryptoManagerException {
    // Initial decomposition.
@@ -983,7 +1004,9 @@
    }
    // Confirm key can be unwrapped at this instance.
    final String instanceKeyID = getInstanceKeyID();
    final byte[] instanceKeyCertificate =
            getInstanceKeyCertificateFromLocalTruststore();
    final String instanceKeyID = getInstanceKeyID(instanceKeyCertificate);
    if (! wrappingKeyIDElement.equals(instanceKeyID)) {
      return null;
    }
@@ -1008,11 +1031,25 @@
    // Unwrap secret key.
    SecretKey secretKey;
    try {
      final Cipher unwrapper
              = Cipher.getInstance(wrappingTransformationElement);
      unwrapper.init(Cipher.UNWRAP_MODE, privateKey);
      secretKey = (SecretKey)unwrapper.unwrap(wrappedKeyCipherTextElement,
              wrappedKeyAlgorithmElement, Cipher.SECRET_KEY);
      if (RsaSveKeyTransport.isScheme(wrappingTransformationElement)) {
        // The public key comes from the instance key certificate whose
        // identifier was just matched: a private key held by a token does
        // not always expose its modulus.
        final Certificate certificate = CertificateFactory.getInstance("X.509")
                .generateCertificate(
                        new ByteArrayInputStream(instanceKeyCertificate));
        secretKey = RsaSveKeyTransport.unwrap(
                new KeyPair(certificate.getPublicKey(), privateKey),
                wrappedKeyCipherTextElement, wrappedKeyAlgorithmElement,
                wrappingKeyIDElement);
      }
      else {
        final Cipher unwrapper
                = Cipher.getInstance(wrappingTransformationElement);
        unwrapper.init(Cipher.UNWRAP_MODE, privateKey);
        secretKey = (SecretKey)unwrapper.unwrap(wrappedKeyCipherTextElement,
                wrappedKeyAlgorithmElement, Cipher.SECRET_KEY);
      }
    } catch(GeneralSecurityException ex) {
      logger.traceException(ex);
      throw new CryptoManagerException(
opendj-server-legacy/src/main/java/org/opends/server/crypto/RsaSveKeyTransport.java
New file
@@ -0,0 +1,402 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.crypto;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;
import java.util.Set;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
/**
 * The key wrapping scheme for a Java runtime which offers no RSA-OAEP, such as one whose only
 * provider is SunPKCS11: a scheme of OpenDJ's own, built on the hybrid key transport of NIST
 * SP 800-56B Rev. 2, section 9.3, but not the KAS1-basic scheme to the letter, since no nonce of
 * the receiving instance enters the derivation. The RSA secret value encapsulation (RSASVE,
 * section 7.2.1) establishes a secret value with the public key of the receiving instance, the
 * one-step key derivation of NIST SP 800-56C Rev. 2 over SHA-256 turns it into a key wrapping key,
 * and the AES key wrap with padding of NIST SP 800-38F (KWP) wraps the secret key with that key.
 * The runtime has to offer a raw RSA cipher, a SHA-256 digest and AES-KWP, nothing more.
 * <p>
 * A wrapped key is the encapsulated secret value, on as many bytes as the RSA modulus and preceded
 * by that length on two bytes, followed by the KWP-wrapped secret key. The derivation binds the
 * scheme name, the identifier of the instance key the secret value is encapsulated with and the
 * algorithm of the wrapped key, the fields stored next to the wrapped key in a ds-cfg-symmetric-key
 * value, so that none of them can be changed without the unwrapping failing. No nonce of the
 * receiving instance enters the derivation: keys are wrapped for an instance from its published
 * certificate alone, without the instance taking part.
 */
final class RsaSveKeyTransport
{
  /**
   * The name of the scheme: the value of the key-wrapping-transformation property which selects
   * it, and the transformation recorded in the ds-cfg-symmetric-key values it produces.
   */
  static final String NAME = "RSASVE/KDF-SHA256/AESKWP";
  private static final String RSA_TRANSFORMATION = "RSA/ECB/NoPadding";
  private static final String DIGEST_ALGORITHM = "SHA-256";
  /**
   * The names of AES-KWP: first the only one Bouncy Castle FIPS knows, so that it does the key wrap
   * when it is installed first, as in the FIPS mode of OpenDJ, even where the Java runtime offers
   * AES-KWP too; then the standard one.
   */
  private static final String[] KEY_WRAP_TRANSFORMATIONS = { "AESWrapPad", "AES/KWP/NoPadding" };
  /** The length in bits of the key wrapping key: one SHA-256 hash. */
  private static final int KEY_WRAPPING_KEY_LENGTH_BITS = 256;
  /** The counter of the only hash the key derivation computes. */
  private static final byte[] KDF_COUNTER = { 0, 0, 0, 1 };
  /** The object identifier of the key usage extension of an X.509 certificate. */
  private static final String KEY_USAGE_OID = "2.5.29.15";
  /** The position of keyEncipherment in the key usage of an X.509 certificate. */
  private static final int KEY_ENCIPHERMENT = 2;
  private RsaSveKeyTransport()
  {
    // Not instantiable.
  }
  /**
   * Returns whether a key wrapping transformation names this scheme. Like the names of the Java
   * transformations, the name is not case sensitive.
   *
   * @param transformation
   *          The key wrapping transformation.
   * @return Whether it names this scheme.
   */
  static boolean isScheme(final String transformation)
  {
    return NAME.equalsIgnoreCase(transformation);
  }
  /**
   * Checks that this Java runtime offers every algorithm the scheme needs.
   *
   * @throws GeneralSecurityException
   *           If it does not, naming the first algorithm missing.
   */
  static void checkSupported() throws GeneralSecurityException
  {
    Cipher.getInstance(RSA_TRANSFORMATION);
    MessageDigest.getInstance(DIGEST_ALGORITHM);
    getKeyWrapCipher();
  }
  /**
   * Wraps a secret key for the instance which holds the private key of a certificate. Like
   * {@code Cipher.init(Cipher.WRAP_MODE, certificate)}, it refuses an X.509 certificate whose key
   * usage extension is critical and does not allow keyEncipherment.
   *
   * @param certificate
   *          The certificate of the instance, with its RSA public key.
   * @param secretKey
   *          The secret key to wrap.
   * @param wrappingKeyID
   *          The identifier of the instance key, as stored next to the wrapped key.
   * @param random
   *          The random generator of the secret value.
   * @return The wrapped key.
   * @throws GeneralSecurityException
   *           If the key usage of the certificate does not allow wrapping keys, if its public key
   *           is not an RSA key, or if the runtime cannot wrap the key.
   */
  static byte[] wrap(final Certificate certificate, final SecretKey secretKey, final String wrappingKeyID,
      final SecureRandom random) throws GeneralSecurityException
  {
    if (certificate instanceof X509Certificate)
    {
      final X509Certificate x509Certificate = (X509Certificate) certificate;
      final Set<String> criticalExtensions = x509Certificate.getCriticalExtensionOIDs();
      final boolean[] keyUsage = x509Certificate.getKeyUsage();
      if (criticalExtensions != null && criticalExtensions.contains(KEY_USAGE_OID)
          && keyUsage != null && keyUsage.length > KEY_ENCIPHERMENT && !keyUsage[KEY_ENCIPHERMENT])
      {
        throw new InvalidKeyException(NAME + " needs a certificate whose key usage allows keyEncipherment");
      }
    }
    return wrap(certificate.getPublicKey(), secretKey, wrappingKeyID, random);
  }
  /**
   * Wraps a secret key for the instance which holds the private key of an RSA public key.
   *
   * @param publicKey
   *          The RSA public key of the instance.
   * @param secretKey
   *          The secret key to wrap.
   * @param wrappingKeyID
   *          The identifier of the instance key, as stored next to the wrapped key.
   * @param random
   *          The random generator of the secret value.
   * @return The wrapped key.
   * @throws GeneralSecurityException
   *           If the public key is not an RSA key, or if the runtime cannot wrap the key.
   */
  static byte[] wrap(final PublicKey publicKey, final SecretKey secretKey, final String wrappingKeyID,
      final SecureRandom random) throws GeneralSecurityException
  {
    final BigInteger modulus = getModulus(publicKey);
    final int length = (modulus.bitLength() + 7) / 8;
    final byte[] secretValue = generateSecretValue(modulus, length, random);
    try
    {
      final Cipher rsa = Cipher.getInstance(RSA_TRANSFORMATION);
      rsa.init(Cipher.ENCRYPT_MODE, publicKey);
      // On as many bytes as the modulus whatever the provider: the unwrapping takes the length of
      // the encapsulated value for the length of the secret value.
      final byte[] encapsulated = toLength(rsa.doFinal(secretValue), length);
      final Cipher keyWrap = getKeyWrapCipher();
      keyWrap.init(Cipher.WRAP_MODE, deriveKeyWrappingKey(secretValue, wrappingKeyID, secretKey.getAlgorithm()));
      final byte[] wrapped = keyWrap.wrap(secretKey);
      return ByteBuffer.allocate(2 + encapsulated.length + wrapped.length)
          .putShort((short) encapsulated.length)
          .put(encapsulated)
          .put(wrapped)
          .array();
    }
    finally
    {
      Arrays.fill(secretValue, (byte) 0);
    }
  }
  /**
   * Unwraps a secret key which {@link #wrap} wrapped with the public key of this instance.
   * <p>
   * The encapsulated value is checked against the modulus before the private key decrypts it, and
   * every failure from the decryption on is reported with one and the same message, the cause
   * aside: what the private key recovers from a value does not show in how the value is refused.
   * A runtime which lacks one of the ciphers, or whose RSA cipher cannot use the private key, is
   * reported before that, with its own reason.
   *
   * @param instanceKeyPair
   *          The RSA key pair of this instance. The modulus is taken from its public key, since a
   *          private key held by a token does not always expose it.
   * @param wrappedKey
   *          The wrapped key.
   * @param keyAlgorithm
   *          The algorithm of the wrapped key, as stored next to it.
   * @param wrappingKeyID
   *          The identifier of the instance key, as stored next to the wrapped key.
   * @return The secret key.
   * @throws GeneralSecurityException
   *           If the wrapped key is malformed, if it was not wrapped for this key pair with these
   *           fields, or if the runtime cannot unwrap it.
   */
  static SecretKey unwrap(final KeyPair instanceKeyPair, final byte[] wrappedKey, final String keyAlgorithm,
      final String wrappingKeyID) throws GeneralSecurityException
  {
    final PrivateKey privateKey = instanceKeyPair.getPrivate();
    if (privateKey == null)
    {
      throw new InvalidKeyException(NAME + " needs the private key of this instance");
    }
    final BigInteger modulus = getModulus(instanceKeyPair.getPublic());
    if (wrappedKey.length < 2)
    {
      throw new InvalidKeyException(NAME + " wrapped key is truncated");
    }
    final ByteBuffer buffer = ByteBuffer.wrap(wrappedKey);
    final int length = buffer.getShort() & 0xFFFF;
    if (length == 0 || buffer.remaining() <= length)
    {
      throw new InvalidKeyException(NAME + " wrapped key is truncated");
    }
    // RSASVE.RECOVER takes a value on as many bytes as the modulus, and RSADP an integer c with
    // 1 < c < n - 1: a value which is neither does not reach the private key.
    if (length != (modulus.bitLength() + 7) / 8)
    {
      throw new InvalidKeyException(NAME + " encapsulated value is not as long as the RSA modulus");
    }
    final byte[] encapsulated = new byte[length];
    buffer.get(encapsulated);
    final BigInteger c = new BigInteger(1, encapsulated);
    if (c.compareTo(BigInteger.ONE) <= 0 || c.compareTo(modulus.subtract(BigInteger.ONE)) >= 0)
    {
      throw new InvalidKeyException(NAME + " encapsulated value is out of range");
    }
    final byte[] wrapped = new byte[buffer.remaining()];
    buffer.get(wrapped);
    // Neither cipher depends on the value, nor does the private key: a runtime which lacks one of
    // the ciphers, or whose RSA cipher cannot use the private key, is told apart from a value which
    // cannot be unwrapped.
    final Cipher rsa = Cipher.getInstance(RSA_TRANSFORMATION);
    rsa.init(Cipher.DECRYPT_MODE, privateKey);
    final Cipher keyWrap = getKeyWrapCipher();
    byte[] decrypted = null;
    byte[] secretValue = null;
    try
    {
      decrypted = rsa.doFinal(encapsulated);
      // Bouncy Castle FIPS drops the leading zero bytes of the secret value.
      secretValue = toLength(decrypted, length);
      keyWrap.init(Cipher.UNWRAP_MODE, deriveKeyWrappingKey(secretValue, wrappingKeyID, keyAlgorithm));
      return (SecretKey) keyWrap.unwrap(wrapped, keyAlgorithm, Cipher.SECRET_KEY);
    }
    catch (GeneralSecurityException | RuntimeException e)
    {
      throw new InvalidKeyException(NAME + " cannot unwrap the key", e);
    }
    finally
    {
      if (decrypted != null)
      {
        Arrays.fill(decrypted, (byte) 0);
      }
      if (secretValue != null)
      {
        Arrays.fill(secretValue, (byte) 0);
      }
    }
  }
  /** Returns the modulus of an RSA public key, refusing any other key. */
  private static BigInteger getModulus(final PublicKey publicKey) throws InvalidKeyException
  {
    if (!(publicKey instanceof RSAPublicKey))
    {
      throw new InvalidKeyException(
          NAME + " needs an RSA public key, not " + (publicKey != null ? publicKey.getAlgorithm() : null));
    }
    return ((RSAPublicKey) publicKey).getModulus();
  }
  /**
   * Draws the secret value of RSASVE.GENERATE: a random integer z with 1 < z < n - 1, on as many
   * bytes as the modulus n. A draw outside the interval is discarded and drawn again.
   */
  private static byte[] generateSecretValue(final BigInteger modulus, final int length, final SecureRandom random)
  {
    final BigInteger upperBound = modulus.subtract(BigInteger.ONE);
    final byte[] secretValue = new byte[length];
    while (true)
    {
      random.nextBytes(secretValue);
      final BigInteger z = new BigInteger(1, secretValue);
      if (z.compareTo(BigInteger.ONE) > 0 && z.compareTo(upperBound) < 0)
      {
        return secretValue;
      }
    }
  }
  /**
   * Returns the unsigned big-endian integer of the bytes on exactly {@code length} bytes, the way
   * NIST SP 800-56B converts an integer to a byte string: leading zero bytes are added or dropped.
   */
  private static byte[] toLength(final byte[] bytes, final int length) throws InvalidKeyException
  {
    int start = 0;
    while (bytes.length - start > length && bytes[start] == 0)
    {
      start++;
    }
    final int significant = bytes.length - start;
    if (significant > length)
    {
      throw new InvalidKeyException(NAME + " value is longer than the RSA modulus");
    }
    if (significant == length)
    {
      return start == 0 ? bytes : Arrays.copyOfRange(bytes, start, bytes.length);
    }
    final byte[] result = new byte[length];
    System.arraycopy(bytes, start, result, length - significant, significant);
    return result;
  }
  /**
   * Derives the key wrapping key with the one-step key derivation of NIST SP 800-56C Rev. 2, with
   * SHA-256 as the auxiliary function: one hash of the counter 1, the secret value and the fixed
   * information yields its 256 bits.
   */
  private static SecretKey deriveKeyWrappingKey(final byte[] secretValue, final String wrappingKeyID,
      final String keyAlgorithm) throws NoSuchAlgorithmException
  {
    final MessageDigest digest = MessageDigest.getInstance(DIGEST_ALGORITHM);
    digest.update(KDF_COUNTER);
    digest.update(secretValue);
    digest.update(fixedInfo(wrappingKeyID, keyAlgorithm));
    final byte[] key = digest.digest();
    try
    {
      return new SecretKeySpec(key, "AES");
    }
    finally
    {
      Arrays.fill(key, (byte) 0);
    }
  }
  /**
   * Returns the fixed information of the key derivation, each variable-length field preceded by
   * its length on four bytes: the scheme name as AlgorithmID; an empty PartyUInfo, since the
   * wrapping instance is not identified; the identifier of the instance key as PartyVInfo; then,
   * as SuppPubInfo, the algorithm of the wrapped key and the length in bits of the key wrapping key.
   */
  private static byte[] fixedInfo(final String wrappingKeyID, final String keyAlgorithm)
  {
    final byte[] algorithmID = NAME.getBytes(UTF_8);
    final byte[] partyVInfo = wrappingKeyID.getBytes(UTF_8);
    final byte[] wrappedKeyAlgorithm = keyAlgorithm.getBytes(UTF_8);
    return ByteBuffer.allocate(5 * 4 + algorithmID.length + partyVInfo.length + wrappedKeyAlgorithm.length)
        .putInt(algorithmID.length).put(algorithmID)
        .putInt(0)
        .putInt(partyVInfo.length).put(partyVInfo)
        .putInt(wrappedKeyAlgorithm.length).put(wrappedKeyAlgorithm)
        .putInt(KEY_WRAPPING_KEY_LENGTH_BITS)
        .array();
  }
  /**
   * Returns an AES-KWP cipher under the first of its names the runtime knows. When it knows none,
   * the failure names the standard one.
   */
  static Cipher getKeyWrapCipher() throws GeneralSecurityException
  {
    GeneralSecurityException lastFailure = null;
    for (final String transformation : KEY_WRAP_TRANSFORMATIONS)
    {
      try
      {
        return Cipher.getInstance(transformation);
      }
      catch (NoSuchAlgorithmException | NoSuchPaddingException e)
      {
        lastFailure = e;
      }
    }
    throw lastFailure;
  }
}
opendj-server-legacy/src/test/java/org/opends/server/TestCaseUtils.java
@@ -83,6 +83,7 @@
import java.util.logging.Logger;
import com.forgerock.opendj.ldap.tools.LDAPSearch;
import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.dsconfig.DSConfig;
import org.forgerock.opendj.config.server.ConfigException;
@@ -386,6 +387,63 @@
    }
  }
  /**
   * Runs {@code action} on a runtime which offers raw RSA and SHA-256 but no AES-KWP, as Java 11
   * does without Bouncy Castle FIPS. Where a provider offers {@code AES/KWP/NoPadding}, it is
   * withdrawn with {@link #withoutJceService}, and its {@code AESWrapPad} alias with it; since that
   * provider is SunJCE, a stand-in gives back the raw RSA cipher of SunJCE for the duration.
   *
   * @param action
   *          What to run without AES-KWP.
   * @throws Exception
   *           If the action fails.
   */
  public static void withoutKeyWrap(final Callable<Void> action) throws Exception
  {
    if (Security.getProviders("Cipher.AES/KWP/NoPadding") == null)
    {
      action.call();
    }
    else
    {
      final Provider rawRsa = new Provider("RawRsaOfSunJce", "1.0", "RSA cipher of SunJCE") {};
      rawRsa.put("Cipher.RSA", "com.sun.crypto.provider.RSACipher");
      withoutJceService("Cipher", "AES/KWP/NoPadding", action, rawRsa);
    }
  }
  /**
   * Runs {@code action} with the Bouncy Castle FIPS provider installed first, in its default mode,
   * and puts the providers back as they were afterwards.
   *
   * @param <T>
   *          The type of what the action returns.
   * @param action
   *          What to run.
   * @return What the action returns.
   * @throws Exception
   *           If the action fails.
   */
  public static <T> T withBcFipsFirst(final Callable<T> action) throws Exception
  {
    final List<Provider> installed = Arrays.asList(Security.getProviders());
    final Provider previous = Security.getProvider(BouncyCastleFipsProvider.PROVIDER_NAME);
    Security.removeProvider(BouncyCastleFipsProvider.PROVIDER_NAME);
    Security.insertProviderAt(previous != null ? previous : new BouncyCastleFipsProvider(), 1);
    try
    {
      return action.call();
    }
    finally
    {
      Security.removeProvider(BouncyCastleFipsProvider.PROVIDER_NAME);
      if (previous != null)
      {
        Security.insertProviderAt(previous, installed.indexOf(previous) + 1);
      }
    }
  }
  public static void startServer() throws Exception
  {
    System.setProperty(PROPERTY_RUNNING_UNIT_TESTS, "true");
opendj-server-legacy/src/test/java/org/opends/server/crypto/CryptoManagerTestCase.java
@@ -37,18 +37,26 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Proxy;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.AlgorithmParameters;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.Security;
import java.security.spec.AlgorithmParameterSpec;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -66,15 +74,21 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.crypto.Cipher;
import javax.crypto.CipherSpi;
import javax.crypto.KeyGeneratorSpi;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.crypto.CryptoServicesRegistrar;
import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.ldap.Attribute;
@@ -879,6 +893,223 @@
    }, sunWithoutMd5);
  }
  @DataProvider
  public Object[][] rsaSveSpellings()
  {
    return new Object[][] { { RsaSveKeyTransport.NAME }, { RsaSveKeyTransport.NAME.toLowerCase() } };
  }
  /**
   The RSASVE scheme is accepted where the runtime offers its algorithms, whatever the case of its
   name, as a Java transformation would be: neither the support check nor the trial wrap of the
   check asks the runtime for a cipher of that name, which no provider offers.
   */
  @Test(dataProvider = "rsaSveSpellings")
  public void testRsaSveKeyWrappingIsAccepted(final String spelling) throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
    final List<LocalizableMessage> why = new ArrayList<>();
    final boolean acceptable = withBcFipsFirst(() -> cm.isConfigurationChangeAcceptable(
        withProperty(cfg, "getKeyWrappingTransformation", spelling), why));
    assertThat(why).isEmpty();
    assertThat(acceptable).isTrue();
  }
  /**
   The RSASVE scheme is refused where the runtime lacks one of its algorithms, here AES-KWP as on
   Java 11 without Bouncy Castle FIPS, and the refusal names the property to set and the missing
   algorithm: the trial wrap which follows the support check would refuse it too, but without
   naming the property.
   */
  @Test
  public void testRsaSveKeyWrappingWithoutAesKwpIsRefusedNamingTheProperty() throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
    final List<LocalizableMessage> why = new ArrayList<>();
    withoutKeyWrap(() ->
    {
      // SunJCE offers AES-KWP under that name too, and goes with AES/KWP/NoPadding.
      assertThat(Security.getProviders("Cipher.AESWrapPad")).as("providers offering AESWrapPad").isNull();
      assertThat(cm.isConfigurationChangeAcceptable(
          withProperty(cfg, "getKeyWrappingTransformation", RsaSveKeyTransport.NAME), why)).isFalse();
      return null;
    });
    assertThat(why).hasSize(1);
    assertThat(why.get(0).ordinal()).isEqualTo(ERR_CRYPTOMGR_KEY_WRAPPING_TRANSFORMATION_UNSUPPORTED.ordinal());
    assertThat(why.get(0).toString())
        .contains(RsaSveKeyTransport.NAME).contains("key-wrapping-transformation").contains("AES/KWP/NoPadding");
  }
  /**
   A key generated while the RSASVE scheme is configured is published wrapped with it, under its
   name, and unwraps to the key the data was encrypted with.
   */
  @Test
  public void testRsaSveWrappedKeyIsPublishedAndUnwraps() throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
    final CryptoManagerCfg rsaSve = withProperty(cfg, "getKeyWrappingTransformation", RsaSveKeyTransport.NAME);
    // No other test generates a key for this cipher and key length: the key is generated here.
    final String transformation = "AES/CBC/PKCS5Padding";
    final int keyLengthBits = 192;
    final byte[] data = "RSASVE key transport".getBytes(StandardCharsets.UTF_8);
    final String instanceKeyID = cm.getInstanceKeyID();
    // A key of the default cipher, wrapped with the default transformation, RSA-OAEP.
    cm.encrypt(data);
    final String oaepValue =
        getValueWrappedFor(instanceKeyID, cfg.getCipherTransformation(), cfg.getCipherKeyLength());
    assertThat(oaepValue.split(":")[1]).isEqualTo(cfg.getKeyWrappingTransformation());
    final byte[] oaepKey = cm.decodeSymmetricKeyAttribute(oaepValue).getEncoded();
    withBcFipsFirst(() ->
    {
      final List<LocalizableMessage> why = new ArrayList<>();
      assertThat(cm.isConfigurationChangeAcceptable(rsaSve, why)).as("%s", why).isTrue();
      cm.applyConfigurationChange(rsaSve);
      final String value;
      final SecretKey key;
      try
      {
        final byte[] cipherText = cm.encrypt(transformation, keyLengthBits, data);
        value = getValueWrappedFor(instanceKeyID, transformation, keyLengthBits);
        assertThat(value.split(":")[1]).isEqualTo(RsaSveKeyTransport.NAME);
        key = cm.decodeSymmetricKeyAttribute(value);
        // The cipher text is the prologue version, the 16-byte key identifier, the IV, then the data.
        final int ivOffset = 1 + 16;
        final int ivLength = 16;
        final Cipher cipher = Cipher.getInstance(transformation);
        cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(cipherText, ivOffset, ivLength));
        assertThat(cipher.doFinal(cipherText, ivOffset + ivLength, cipherText.length - ivOffset - ivLength))
            .isEqualTo(data);
        assertThat(cm.decodeSymmetricKeyAttribute(oaepValue).getEncoded())
            .as("RSA-OAEP value while the RSASVE scheme is configured").isEqualTo(oaepKey);
      }
      finally
      {
        cm.applyConfigurationChange(cfg);
        // The admin data backend keeps the key across restarts, where a runtime without AES-KWP,
        // such as Java 11 without Bouncy Castle FIPS, cannot unwrap it and logs an error for it.
        for (final Entry entry : getKeysForCipher(transformation, keyLengthBits))
        {
          deleteEntry(entry);
        }
      }
      // A value is unwrapped by the transformation it records, whatever the configured one, and
      // the scheme name is not case sensitive there either.
      assertThat(cm.decodeSymmetricKeyAttribute(value).getEncoded())
          .as("RSASVE value while RSA-OAEP is configured").isEqualTo(key.getEncoded());
      final String lowerCaseValue =
          value.replace(":" + RsaSveKeyTransport.NAME + ":", ":" + RsaSveKeyTransport.NAME.toLowerCase() + ":");
      assertThat(cm.decodeSymmetricKeyAttribute(lowerCaseValue).getEncoded())
          .as("RSASVE value recording the scheme name in lower case").isEqualTo(key.getEncoded());
      return null;
    });
  }
  @DataProvider
  public Object[][] keyUsages()
  {
    final String oaep = "RSA/ECB/OAEPWITHSHA-1ANDMGF1PADDING";
    final int signing = KeyUsage.digitalSignature;
    final int wrapping = KeyUsage.digitalSignature | KeyUsage.keyEncipherment;
    // Transformation, key usage, whether it is critical, whether a key is wrapped.
    return new Object[][] {
      { oaep, signing, true, false },
      { RsaSveKeyTransport.NAME, signing, true, false },
      { RsaSveKeyTransport.NAME, wrapping, true, true },
      { RsaSveKeyTransport.NAME, signing, false, true },
    };
  }
  /**
   No secret key is wrapped for an instance whose certificate has a critical key usage without
   keyEncipherment, whatever the transformation: Cipher.init refuses such a certificate for
   RSA-OAEP, and the RSASVE scheme, which takes the public key alone, checks the key usage the same
   way. A key usage which is not critical restricts nothing.
   */
  @Test(dataProvider = "keyUsages")
  public void testSecretKeyIsWrappedOnlyWhereTheKeyUsageAllowsIt(final String transformation, final int keyUsage,
      final boolean critical, final boolean wrapped) throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
    final CryptoManagerCfg withTransformation = withProperty(cfg, "getKeyWrappingTransformation", transformation);
    cm.encrypt("key usage".getBytes(StandardCharsets.UTF_8));
    final String value =
        getValueWrappedFor(cm.getInstanceKeyID(), cfg.getCipherTransformation(), cfg.getCipherKeyLength());
    final byte[] certificate = rsaCertificate(keyUsage, critical);
    final String keyID = CryptoManagerImpl.getInstanceKeyID(certificate);
    final DN instanceKey = DN.valueOf(ATTR_CRYPTO_KEY_ID + "=" + keyID + ",cn=instance keys,cn=admin data");
    final DN trustStoreEntry = DN.valueOf(DN_TRUST_STORE_ROOT).child(instanceKey.rdn());
    withBcFipsFirst(() ->
    {
      final List<LocalizableMessage> why = new ArrayList<>();
      assertThat(cm.isConfigurationChangeAcceptable(withTransformation, why)).as("%s", why).isTrue();
      cm.applyConfigurationChange(withTransformation);
      try
      {
        assertThat(getRootConnection().processAdd(instanceKeyEntry(instanceKey, ByteString.wrap(certificate)))
            .getResultCode()).isEqualTo(ResultCode.SUCCESS);
        if (wrapped)
        {
          assertThat(cm.reencodeSymmetricKeyAttribute(value, keyID)).startsWith(keyID + ":" + transformation + ":");
        }
        else
        {
          assertThatThrownBy(() -> cm.reencodeSymmetricKeyAttribute(value, keyID))
              .isInstanceOf(CryptoManagerException.class)
              .cause().isInstanceOf(InvalidKeyException.class).hasMessageContaining("key usage");
        }
      }
      finally
      {
        cm.applyConfigurationChange(cfg);
        getRootConnection().processDelete(instanceKey);
        getRootConnection().processDelete(trustStoreEntry);
      }
      return null;
    });
  }
  /** Returns a self-signed RSA certificate, DER encoded, with the given key usage. */
  private static byte[] rsaCertificate(final int keyUsage, final boolean critical) throws Exception
  {
    final KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", "SunRsaSign");
    generator.initialize(2048);
    final KeyPair keyPair = generator.generateKeyPair();
    final Instant now = Instant.now();
    final X500Name subject = new X500Name("CN=key usage " + keyUsage);
    final JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(subject,
        BigInteger.valueOf(now.toEpochMilli()), Date.from(now.minus(1, ChronoUnit.DAYS)),
        Date.from(now.plus(1, ChronoUnit.DAYS)), subject, keyPair.getPublic());
    builder.addExtension(Extension.keyUsage, critical, new KeyUsage(keyUsage));
    return builder.build(new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate())).getEncoded();
  }
  /** Returns the ds-cfg-symmetric-key value of the key of a cipher which is wrapped for an instance. */
  private String getValueWrappedFor(final String instanceKeyID, final String transformation,
      final int keyLengthBits) throws DirectoryException
  {
    final Set<String> values = getKeyForCipher(transformation, keyLengthBits)
        .parseAttribute(ATTR_CRYPTO_SYMMETRIC_KEY).asSetOfString();
    return values.stream()
        .filter(v -> v.startsWith(instanceKeyID + ":"))
        .findFirst()
        .orElseThrow(() -> new AssertionError("no value wrapped for this instance in " + values));
  }
  /** The error the probe provider fails with, set by the case using it. */
  private static volatile Error probeError;
@@ -1057,11 +1288,7 @@
   */
  private static <T> T inApprovedOnlyBcFipsThread(final Callable<T> action) throws Exception
  {
    final List<Provider> installed = Arrays.asList(Security.getProviders());
    final Provider previous = Security.getProvider("BCFIPS");
    Security.removeProvider("BCFIPS");
    Security.insertProviderAt(previous != null ? previous : new BouncyCastleFipsProvider(), 1);
    try
    return withBcFipsFirst(() ->
    {
      final FutureTask<T> task = new FutureTask<>(() ->
      {
@@ -1077,15 +1304,7 @@
      {
        throw new AssertionError("the action failed in approved-only mode", e.getCause());
      }
    }
    finally
    {
      Security.removeProvider("BCFIPS");
      if (previous != null)
      {
        Security.insertProviderAt(previous, installed.indexOf(previous) + 1);
      }
    }
    });
  }
  /**
@@ -1128,11 +1347,16 @@
  private Entry getKeyForCipher(String cipher, int keyLength) throws DirectoryException
  {
    return getKeysForCipher(cipher, keyLength).get(0);
  }
  private List<? extends Entry> getKeysForCipher(String cipher, int keyLength) throws DirectoryException
  {
    SearchRequest request = newSearchRequest("cn=secret keys, cn=admin data", SearchScope.WHOLE_SUBTREE,
        "&(ds-cfg-cipher-transformation-name=" + cipher + ")(ds-cfg-key-length-bits=" + keyLength + ")");
    InternalClientConnection conn = getRootConnection();
    InternalSearchOperation search = conn.processSearch(request);
    return search.getSearchEntries().get(0);
    return search.getSearchEntries();
  }
  @Test
opendj-server-legacy/src/test/java/org/opends/server/crypto/RsaSveKeyTransportTestCase.java
New file
@@ -0,0 +1,688 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.crypto;
import static org.assertj.core.api.Assertions.*;
import static org.opends.server.TestCaseUtils.*;
import java.io.ByteArrayOutputStream;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.AlgorithmParameters;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.interfaces.RSAKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
import javax.crypto.Cipher;
import javax.crypto.CipherSpi;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider;
import org.opends.server.util.StaticUtils;
import org.testng.SkipException;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
/** Tests the RSASVE key transport scheme of the crypto manager. */
@SuppressWarnings("javadoc")
public class RsaSveKeyTransportTestCase extends CryptoTestCase
{
  private static final String KEY_ID = "0123456789ABCDEF0123456789ABCDEF";
  /** The providers of the Java runtime, which offer AES/KWP/NoPadding from Java 17 on. */
  private static final String JDK = "jdk";
  /**
   * Bouncy Castle FIPS first and no provider offering AES/KWP/NoPadding: AES-KWP only under the
   * name AESWrapPad, and a raw RSA which drops the leading zero bytes of what it decrypts.
   */
  private static final String BC_FIPS = "bcfips";
  /** Created before any test installs Bouncy Castle FIPS, so that it comes from the SUN provider. */
  private final SecureRandom random = new SecureRandom();
  private KeyPair keyPair;
  @BeforeClass
  public void generateKeyPair() throws Exception
  {
    keyPair = rsaKeyPair(2048);
  }
  @DataProvider
  public Object[][] providers()
  {
    return new Object[][] { { JDK }, { BC_FIPS } };
  }
  @DataProvider
  public Object[][] keys()
  {
    final Object[][] keys = { { "DES", 8 }, { "AES", 16 }, { "HmacSHA1", 20 }, { "DESede", 24 }, { "AES", 32 } };
    final Object[][] rows = new Object[keys.length * 2][];
    for (int i = 0; i < keys.length; i++)
    {
      rows[2 * i] = new Object[] { JDK, keys[i][0], keys[i][1] };
      rows[2 * i + 1] = new Object[] { BC_FIPS, keys[i][0], keys[i][1] };
    }
    return rows;
  }
  /** Keys of every length unwrap to themselves: the 20 bytes of an HmacSHA1 key need the padding of KWP. */
  @Test(dataProvider = "keys")
  public void testWrappedKeyUnwrapsToTheSameKey(final String providers, final String algorithm, final int length)
      throws Exception
  {
    final SecretKey key = new SecretKeySpec(bytes(length, 0x3C), algorithm);
    inProviders(providers, () ->
    {
      final byte[] wrapped = RsaSveKeyTransport.wrap(keyPair.getPublic(), key, KEY_ID, random);
      final SecretKey unwrapped = RsaSveKeyTransport.unwrap(keyPair, wrapped, algorithm, KEY_ID);
      assertThat(unwrapped.getAlgorithm()).isEqualTo(algorithm);
      assertThat(unwrapped.getEncoded()).isEqualTo(key.getEncoded());
      return null;
    });
  }
  /**
   * The key wrapping key is derived from the encapsulated secret value: another private key
   * recovers another value and cannot unwrap. The other modulus is the larger one, so that the
   * encapsulated value is in its range and its private key decrypts it instead of refusing it.
   */
  @Test
  public void testAnotherPrivateKeyCannotUnwrap() throws Exception
  {
    KeyPair other;
    do
    {
      other = rsaKeyPair(2048);
    }
    while (modulus(other).compareTo(modulus(keyPair)) <= 0);
    final KeyPair otherKeyPair = other;
    final SecretKey key = new SecretKeySpec(bytes(16, 0x3C), "AES");
    inProviders(BC_FIPS, () ->
    {
      final byte[] wrapped = RsaSveKeyTransport.wrap(keyPair.getPublic(), key, KEY_ID, random);
      assertThatThrownBy(() -> RsaSveKeyTransport.unwrap(otherKeyPair, wrapped, "AES", KEY_ID))
          .isInstanceOf(GeneralSecurityException.class);
      return null;
    });
  }
  /** The derivation binds the fields stored next to the wrapped key: changing either one fails the unwrapping. */
  @Test
  public void testFieldsStoredNextToTheWrappedKeyAreBound() throws Exception
  {
    final SecretKey key = new SecretKeySpec(bytes(16, 0x3C), "AES");
    inProviders(BC_FIPS, () ->
    {
      final byte[] wrapped = RsaSveKeyTransport.wrap(keyPair.getPublic(), key, KEY_ID, random);
      assertThatThrownBy(() -> RsaSveKeyTransport.unwrap(keyPair, wrapped, "AES", KEY_ID.toLowerCase()))
          .as("another instance key identifier").isInstanceOf(GeneralSecurityException.class);
      assertThatThrownBy(() -> RsaSveKeyTransport.unwrap(keyPair, wrapped, "HmacSHA256", KEY_ID))
          .as("another key algorithm").isInstanceOf(GeneralSecurityException.class);
      return null;
    });
  }
  /**
   * The wrapped key of a known secret value. What this version stores in cn=secret keys is what
   * every later version has to unwrap, so the derivation and the key wrap are pinned to a value
   * computed independently of this implementation: SHA-256 of the counter 1, the secret value and
   * the fixed information, then AES-KWP of the key with it. The key wrap of both the Java runtime and
   * Bouncy Castle FIPS has to give it.
   */
  @Test(dataProvider = "providers")
  public void testKnownAnswer(final String providers) throws Exception
  {
    final byte[] secretValue = new byte[256];
    for (int i = 0; i < secretValue.length; i++)
    {
      secretValue[i] = (byte) (i + 1);
    }
    final Draws draws = new Draws(secretValue);
    final SecretKey key =
        new SecretKeySpec(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, "AES");
    inProviders(providers, () ->
    {
      final byte[] wrapped = RsaSveKeyTransport.wrap(keyPair.getPublic(), key, KEY_ID, draws);
      assertThat(ByteBuffer.wrap(wrapped).getShort()).isEqualTo((short) 256);
      final byte[] wrappedKey = Arrays.copyOfRange(wrapped, 2 + 256, wrapped.length);
      assertThat(StaticUtils.bytesToHexNoSpace(wrappedKey))
          .isEqualToIgnoringCase("A29EDD3A5DDBD441B69FB866DDE179CF6CC59C86B54A1AF5");
      return null;
    });
  }
  @DataProvider
  public Object[][] secretValuesOutsideTheInterval()
  {
    return new Object[][] {
      { "0", new byte[256] },
      { "1", toBytes(BigInteger.ONE) },
      { "n - 1", toBytes(modulus(keyPair).subtract(BigInteger.ONE)) },
      { "2^2048 - 1", bytes(256, 0xFF) } };
  }
  /** RSASVE.GENERATE draws z with 1 < z < n - 1: a value outside is discarded and drawn again. */
  @Test(dataProvider = "secretValuesOutsideTheInterval")
  public void testSecretValueOutsideTheIntervalIsDrawnAgain(final String value, final byte[] outside)
      throws Exception
  {
    final Draws draws = new Draws(outside, bytes(256, 0x5A));
    final SecretKey key = new SecretKeySpec(bytes(16, 0x3C), "AES");
    inProviders(BC_FIPS, () ->
    {
      final byte[] wrapped = RsaSveKeyTransport.wrap(keyPair.getPublic(), key, KEY_ID, draws);
      assertThat(draws.count()).as(value).isEqualTo(2);
      assertThat(RsaSveKeyTransport.unwrap(keyPair, wrapped, "AES", KEY_ID).getEncoded())
          .isEqualTo(key.getEncoded());
      return null;
    });
  }
  /** Bouncy Castle FIPS drops the leading zero bytes of a decrypted secret value, which the derivation needs. */
  @Test
  public void testSecretValueWithALeadingZeroByteUnwraps() throws Exception
  {
    final byte[] secretValue = bytes(256, 0x5A);
    secretValue[0] = 0;
    final Draws draws = new Draws(secretValue);
    final SecretKey key = new SecretKeySpec(bytes(16, 0x3C), "AES");
    inProviders(BC_FIPS, () ->
    {
      final byte[] wrapped = RsaSveKeyTransport.wrap(keyPair.getPublic(), key, KEY_ID, draws);
      assertThat(RsaSveKeyTransport.unwrap(keyPair, wrapped, "AES", KEY_ID).getEncoded())
          .isEqualTo(key.getEncoded());
      return null;
    });
  }
  /**
   * A provider may drop the leading zero bytes of the encapsulated value as well, which the
   * wrapping puts back: the value is stored on as many bytes as the modulus, the only length the
   * unwrapping accepts.
   */
  @Test
  public void testEncapsulatedValueWithALeadingZeroByteKeepsTheModulusLength() throws Exception
  {
    final BigInteger n = modulus(keyPair);
    final BigInteger e = ((RSAPublicKey) keyPair.getPublic()).getPublicExponent();
    final BigInteger upperBound = n.subtract(BigInteger.ONE);
    // About one secret value in 256 has an encapsulated value z^e mod n below 2^2040.
    BigInteger z;
    do
    {
      z = new BigInteger(n.bitLength(), random);
    }
    while (z.compareTo(BigInteger.ONE) <= 0 || z.compareTo(upperBound) >= 0 || z.modPow(e, n).bitLength() > 2040);
    final Draws draws = new Draws(toBytes(z));
    final SecretKey key = new SecretKeySpec(bytes(16, 0x3C), "AES");
    inProviders(BC_FIPS, () ->
    {
      final Provider droppingRsa = new Provider("LeadingZeroDroppingRsa", "1.0", "Raw RSA without leading zeros") {};
      droppingRsa.put("Cipher.RSA", LeadingZeroDroppingRawRsa.class.getName());
      Security.insertProviderAt(droppingRsa, 1);
      try
      {
        final byte[] wrapped = RsaSveKeyTransport.wrap(keyPair.getPublic(), key, KEY_ID, draws);
        assertThat(ByteBuffer.wrap(wrapped).getShort()).isEqualTo((short) 256);
        assertThat(wrapped[2]).isEqualTo((byte) 0);
        assertThat(RsaSveKeyTransport.unwrap(keyPair, wrapped, "AES", KEY_ID).getEncoded())
            .isEqualTo(key.getEncoded());
      }
      finally
      {
        Security.removeProvider(droppingRsa.getName());
      }
      return null;
    });
  }
  /** Nothing assumes a 2048-bit modulus: instance keys generated before 2015 have 1024 bits. */
  @Test
  public void testSmallerModulusRoundTrips() throws Exception
  {
    final KeyPair small = rsaKeyPair(1024);
    final SecretKey key = new SecretKeySpec(bytes(16, 0x3C), "AES");
    inProviders(JDK, () ->
    {
      final byte[] wrapped = RsaSveKeyTransport.wrap(small.getPublic(), key, KEY_ID, random);
      assertThat(ByteBuffer.wrap(wrapped).getShort()).isEqualTo((short) 128);
      assertThat(RsaSveKeyTransport.unwrap(small, wrapped, "AES", KEY_ID).getEncoded())
          .isEqualTo(key.getEncoded());
      return null;
    });
  }
  /** An instance key which is not an RSA key is refused with an exception the crypto manager reports. */
  @Test
  public void testNonRsaPublicKeyIsRefusedWithASecurityException() throws Exception
  {
    final KeyPairGenerator generator = KeyPairGenerator.getInstance("EC");
    generator.initialize(256);
    final PublicKey ecKey = generator.generateKeyPair().getPublic();
    final SecretKey key = new SecretKeySpec(bytes(16, 0x3C), "AES");
    assertThatThrownBy(() -> RsaSveKeyTransport.wrap(ecKey, key, KEY_ID, random))
        .isInstanceOf(InvalidKeyException.class);
  }
  /**
   * The unwrapping refuses an instance key pair without its private key, as a trust store entry
   * holding only a certificate gives, or whose public key is not an RSA key, with an exception the
   * crypto manager reports and before any cipher.
   */
  @Test
  public void testUnusableInstanceKeyPairIsRefusedWithASecurityException() throws Exception
  {
    final byte[] wrapped = { 0, 3, 1, 2, 3, 4 };
    assertThatThrownBy(() -> RsaSveKeyTransport.unwrap(new KeyPair(keyPair.getPublic(), null), wrapped, "AES", KEY_ID))
        .isInstanceOf(InvalidKeyException.class)
        .hasMessageContaining("needs the private key");
    final KeyPairGenerator generator = KeyPairGenerator.getInstance("EC");
    generator.initialize(256);
    final KeyPair ecKeyPair = new KeyPair(generator.generateKeyPair().getPublic(), keyPair.getPrivate());
    assertThatThrownBy(() -> RsaSveKeyTransport.unwrap(ecKeyPair, wrapped, "AES", KEY_ID))
        .isInstanceOf(InvalidKeyException.class)
        .hasMessageContaining("needs an RSA public key");
  }
  @DataProvider
  public Object[][] malformedWrappedKeys()
  {
    return new Object[][] {
      { "empty", new byte[0] },
      { "one byte", new byte[] { 1 } },
      { "zero length", new byte[] { 0, 0, 1, 2, 3 } },
      { "length beyond the end", new byte[] { 0, 9, 1, 2, 3 } },
      { "no wrapped key after the encapsulated value", new byte[] { 0, 3, 1, 2, 3 } } };
  }
  /** A malformed value in cn=secret keys is refused with an exception the crypto manager reports. */
  @Test(dataProvider = "malformedWrappedKeys")
  public void testMalformedWrappedKeyIsRefusedWithASecurityException(final String what, final byte[] wrapped)
  {
    assertThatThrownBy(() -> RsaSveKeyTransport.unwrap(keyPair, wrapped, "AES", KEY_ID))
        .as(what).isInstanceOf(InvalidKeyException.class).hasMessageContaining("truncated");
  }
  @DataProvider
  public Object[][] encapsulatedValuesRefusedBeforeDecrypting()
  {
    final BigInteger n = modulus(keyPair);
    return new Object[][] {
      { "one byte shorter than the modulus", bytes(255, 0x5A), "not as long as the RSA modulus" },
      { "longer than the modulus", bytes(300, 0x5A), "not as long as the RSA modulus" },
      { "0", new byte[256], "out of range" },
      { "1", toBytes(BigInteger.ONE), "out of range" },
      { "n - 1", toBytes(n.subtract(BigInteger.ONE)), "out of range" },
      { "n", toBytes(n), "out of range" },
      { "2^2048 - 1", bytes(256, 0xFF), "out of range" } };
  }
  /**
   * An encapsulated value which is not on as many bytes as the modulus, or not an integer c with
   * 1 < c < n - 1, is refused before the private key decrypts it, whatever the providers: Bouncy
   * Castle FIPS throws an ArrayIndexOutOfBoundsException for a value longer than the modulus.
   */
  @Test(dataProvider = "encapsulatedValuesRefusedBeforeDecrypting")
  public void testEncapsulatedValueIsCheckedBeforeDecrypting(final String what, final byte[] encapsulated,
      final String message) throws Exception
  {
    final byte[] wrapped = wrappedKey(encapsulated, bytes(24, 0x3C));
    final Callable<Void> unwrap = () ->
    {
      assertThatThrownBy(() -> RsaSveKeyTransport.unwrap(keyPair, wrapped, "AES", KEY_ID))
          .as(what).isInstanceOf(InvalidKeyException.class).hasMessageContaining(message);
      return null;
    };
    unwrap.call();
    inProviders(BC_FIPS, unwrap);
  }
  @DataProvider
  public Object[][] encapsulatedValuesInRange()
  {
    final BigInteger n = modulus(keyPair);
    final Object[][] values = {
      { "2", toBytes(BigInteger.valueOf(2)) },
      { "n - 2", toBytes(n.subtract(BigInteger.valueOf(2))) },
      { "0x5A on every byte", bytes(256, 0x5A) } };
    final Object[][] rows = new Object[values.length * 2][];
    for (int i = 0; i < values.length; i++)
    {
      rows[2 * i] = new Object[] { JDK, values[i][0], values[i][1] };
      rows[2 * i + 1] = new Object[] { BC_FIPS, values[i][0], values[i][1] };
    }
    return rows;
  }
  /**
   * Whatever secret value the private key recovers from an encapsulated value in range, a wrapped
   * key which does not unwrap is refused with one and the same message.
   */
  @Test(dataProvider = "encapsulatedValuesInRange")
  public void testUnwrapFailureHasOneMessage(final String providers, final String what, final byte[] encapsulated)
      throws Exception
  {
    final SecretKey key = new SecretKeySpec(bytes(16, 0x3C), "AES");
    final Callable<Void> unwrap = () ->
    {
      final byte[] genuine = RsaSveKeyTransport.wrap(keyPair.getPublic(), key, KEY_ID, random);
      final byte[] wrapped = wrappedKey(encapsulated, Arrays.copyOfRange(genuine, 2 + 256, genuine.length));
      assertThatThrownBy(() -> RsaSveKeyTransport.unwrap(keyPair, wrapped, "AES", KEY_ID))
          .as(what).isInstanceOf(InvalidKeyException.class)
          .hasMessage(RsaSveKeyTransport.NAME + " cannot unwrap the key");
      return null;
    };
    inProviders(providers, unwrap);
  }
  /**
   * The support check names AES/KWP/NoPadding when the runtime offers raw RSA and SHA-256 but no
   * AES-KWP, as a Java 11 runtime does without Bouncy Castle FIPS.
   */
  @Test
  public void testCheckSupportedNamesTheMissingKeyWrap() throws Exception
  {
    withoutKeyWrap(() ->
    {
      assertThatThrownBy(RsaSveKeyTransport::checkSupported)
          .isInstanceOf(GeneralSecurityException.class)
          .hasMessageContaining("AES/KWP/NoPadding");
      return null;
    });
  }
  /**
   * The unwrapping names AES/KWP/NoPadding on a runtime which lacks it, as a server which does not
   * use the scheme but receives keys wrapped with it does: no cipher depends on the value, so such a
   * runtime is not reported as a value which cannot be unwrapped.
   */
  @Test
  public void testUnwrapNamesTheMissingKeyWrap() throws Exception
  {
    final byte[] wrapped = wrappedKey(toBytes(BigInteger.valueOf(2)), bytes(24, 0x3C));
    withoutKeyWrap(() ->
    {
      assertThatThrownBy(() -> RsaSveKeyTransport.unwrap(keyPair, wrapped, "AES", KEY_ID))
          .isInstanceOf(GeneralSecurityException.class)
          .hasMessageContaining("AES/KWP/NoPadding");
      return null;
    });
  }
  /**
   * The unwrapping reports why the RSA cipher refuses the private key, as a provider which cannot
   * use the key of the trust store does: the private key does not depend on the value either, so
   * such a runtime is not reported as a value which cannot be unwrapped.
   */
  @Test
  public void testUnwrapReportsWhyTheRsaCipherRefusesThePrivateKey() throws Exception
  {
    final KeyPairGenerator generator = KeyPairGenerator.getInstance("EC");
    generator.initialize(256);
    final PrivateKey ecKey = generator.generateKeyPair().getPrivate();
    final Throwable refusal =
        catchThrowable(() -> Cipher.getInstance("RSA/ECB/NoPadding").init(Cipher.DECRYPT_MODE, ecKey));
    assertThat(refusal).isInstanceOf(InvalidKeyException.class);
    final byte[] wrapped = wrappedKey(toBytes(BigInteger.valueOf(2)), bytes(24, 0x3C));
    assertThatThrownBy(() -> RsaSveKeyTransport.unwrap(new KeyPair(keyPair.getPublic(), ecKey), wrapped, "AES", KEY_ID))
        .isInstanceOf(InvalidKeyException.class)
        .hasMessage(refusal.getMessage());
  }
  /**
   * With Bouncy Castle FIPS installed first, as in the FIPS mode of OpenDJ, Bouncy Castle FIPS does
   * the key wrap even where the Java runtime offers AES/KWP/NoPadding too, a name it does not know.
   */
  @Test
  public void testKeyWrapComesFromBcFipsWhenItIsFirst() throws Exception
  {
    assertThat(withBcFipsFirst(() -> RsaSveKeyTransport.getKeyWrapCipher().getProvider().getName()))
        .isEqualTo(BouncyCastleFipsProvider.PROVIDER_NAME);
  }
  /** Runs the action with the providers of the Java runtime, or with AES-KWP from Bouncy Castle FIPS only. */
  private static void inProviders(final String providers, final Callable<Void> action) throws Exception
  {
    final boolean jdkOffersKwp = Security.getProviders("Cipher.AES/KWP/NoPadding") != null;
    if (JDK.equals(providers))
    {
      if (!jdkOffersKwp)
      {
        throw new SkipException("This Java runtime offers no AES/KWP/NoPadding");
      }
      action.call();
    }
    else if (jdkOffersKwp)
    {
      withoutJceService("Cipher", "AES/KWP/NoPadding", action, new BouncyCastleFipsProvider());
    }
    else
    {
      withBcFipsFirst(action);
    }
  }
  private static KeyPair rsaKeyPair(final int bits) throws Exception
  {
    final KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", "SunRsaSign");
    generator.initialize(bits);
    return generator.generateKeyPair();
  }
  private static BigInteger modulus(final KeyPair keyPair)
  {
    return ((RSAPublicKey) keyPair.getPublic()).getModulus();
  }
  private static byte[] bytes(final int length, final int value)
  {
    final byte[] bytes = new byte[length];
    Arrays.fill(bytes, (byte) value);
    return bytes;
  }
  /** Returns a wrapped key: the length of the encapsulated value on two bytes, that value, then the KWP-wrapped key. */
  private static byte[] wrappedKey(final byte[] encapsulated, final byte[] wrapped)
  {
    return ByteBuffer.allocate(2 + encapsulated.length + wrapped.length)
        .putShort((short) encapsulated.length)
        .put(encapsulated)
        .put(wrapped)
        .array();
  }
  /** Returns the value on 256 bytes, big-endian, without the sign byte BigInteger may add. */
  private static byte[] toBytes(final BigInteger value)
  {
    final byte[] bytes = value.toByteArray();
    final byte[] result = new byte[256];
    final int length = Math.min(bytes.length, result.length);
    System.arraycopy(bytes, bytes.length - length, result, result.length - length, length);
    return result;
  }
  /**
   * A random generator which hands out the given values, one per draw, the last one again once
   * they are all drawn, and counts the draws. Created outside any provider fixture, so that its
   * own default generator comes from the SUN provider.
   */
  private static final class Draws extends SecureRandom
  {
    private static final long serialVersionUID = 1L;
    private final byte[][] values;
    private final AtomicInteger draws = new AtomicInteger();
    private Draws(final byte[]... values)
    {
      this.values = values;
    }
    @Override
    public void nextBytes(final byte[] bytes)
    {
      final byte[] value = values[Math.min(draws.getAndIncrement(), values.length - 1)];
      assertThat(value).hasSize(bytes.length);
      System.arraycopy(value, 0, bytes, 0, bytes.length);
    }
    int count()
    {
      return draws.get();
    }
  }
  /**
   * A raw RSA cipher which returns what it encrypts or decrypts on as few bytes as the value needs,
   * without its leading zero bytes.
   */
  public static final class LeadingZeroDroppingRawRsa extends CipherSpi
  {
    private final ByteArrayOutputStream input = new ByteArrayOutputStream();
    private RSAKey key;
    private BigInteger exponent;
    @Override
    protected void engineSetMode(final String mode) throws NoSuchAlgorithmException
    {
      if (!"ECB".equalsIgnoreCase(mode))
      {
        throw new NoSuchAlgorithmException(mode);
      }
    }
    @Override
    protected void engineSetPadding(final String padding) throws NoSuchPaddingException
    {
      if (!"NoPadding".equalsIgnoreCase(padding))
      {
        throw new NoSuchPaddingException(padding);
      }
    }
    @Override
    protected int engineGetBlockSize()
    {
      return 0;
    }
    @Override
    protected int engineGetOutputSize(final int inputLength)
    {
      return (key.getModulus().bitLength() + 7) / 8;
    }
    @Override
    protected byte[] engineGetIV()
    {
      return null;
    }
    @Override
    protected AlgorithmParameters engineGetParameters()
    {
      return null;
    }
    @Override
    protected void engineInit(final int mode, final Key key, final SecureRandom random) throws InvalidKeyException
    {
      if (mode == Cipher.ENCRYPT_MODE && key instanceof RSAPublicKey)
      {
        exponent = ((RSAPublicKey) key).getPublicExponent();
      }
      else if (mode == Cipher.DECRYPT_MODE && key instanceof RSAPrivateKey)
      {
        exponent = ((RSAPrivateKey) key).getPrivateExponent();
      }
      else
      {
        throw new InvalidKeyException("Mode " + mode + " with " + key.getClass().getName());
      }
      this.key = (RSAKey) key;
      input.reset();
    }
    @Override
    protected void engineInit(final int mode, final Key key, final AlgorithmParameterSpec params,
        final SecureRandom random) throws InvalidKeyException
    {
      engineInit(mode, key, random);
    }
    @Override
    protected void engineInit(final int mode, final Key key, final AlgorithmParameters params,
        final SecureRandom random) throws InvalidKeyException
    {
      engineInit(mode, key, random);
    }
    @Override
    protected byte[] engineUpdate(final byte[] in, final int offset, final int length)
    {
      input.write(in, offset, length);
      return new byte[0];
    }
    @Override
    protected int engineUpdate(final byte[] in, final int offset, final int length, final byte[] out,
        final int outOffset)
    {
      input.write(in, offset, length);
      return 0;
    }
    @Override
    protected byte[] engineDoFinal(final byte[] in, final int offset, final int length)
    {
      input.write(in, offset, length);
      final byte[] result = new BigInteger(1, input.toByteArray()).modPow(exponent, key.getModulus()).toByteArray();
      input.reset();
      int start = 0;
      while (start < result.length - 1 && result[start] == 0)
      {
        start++;
      }
      return Arrays.copyOfRange(result, start, result.length);
    }
    @Override
    protected int engineDoFinal(final byte[] in, final int offset, final int length, final byte[] out,
        final int outOffset)
    {
      final byte[] result = engineDoFinal(in, offset, length);
      System.arraycopy(result, 0, out, outOffset, result.length);
      return result.length;
    }
  }
}