/* WolfCryptCipher.java * * Copyright (C) 2006-2026 wolfSSL Inc. * * This file is part of wolfSSL. * * wolfSSL is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * wolfSSL is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA */ package com.wolfssl.provider.jce; import java.util.Arrays; import java.nio.ByteBuffer; import java.io.ByteArrayOutputStream; import javax.crypto.Cipher; import javax.crypto.CipherSpi; import javax.crypto.SecretKey; import javax.crypto.BadPaddingException; import javax.crypto.ShortBufferException; import javax.crypto.IllegalBlockSizeException; import javax.crypto.AEADBadTagException; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.OAEPParameterSpec; import javax.crypto.spec.PSource; import javax.crypto.spec.SecretKeySpec; import java.security.SecureRandom; import java.security.MessageDigest; import java.security.KeyFactory; import java.security.spec.MGF1ParameterSpec; import java.security.AlgorithmParameters; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.InvalidParameterSpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.security.spec.InvalidKeySpecException; import java.security.Key; import java.security.NoSuchAlgorithmException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidParameterException; import java.security.InvalidKeyException; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPrivateCrtKey; import java.security.interfaces.RSAPublicKey; import com.wolfssl.wolfcrypt.Aes; import com.wolfssl.wolfcrypt.AesEcb; import com.wolfssl.wolfcrypt.AesCtr; import com.wolfssl.wolfcrypt.AesOfb; import com.wolfssl.wolfcrypt.AesGcm; import com.wolfssl.wolfcrypt.AesCcm; import com.wolfssl.wolfcrypt.AesCts; import com.wolfssl.wolfcrypt.Des3; import com.wolfssl.wolfcrypt.Rsa; import com.wolfssl.wolfcrypt.Rng; import com.wolfssl.wolfcrypt.Sha256; import com.wolfssl.wolfcrypt.Sha512; import com.wolfssl.wolfcrypt.FeatureDetect; import com.wolfssl.wolfcrypt.WolfCrypt; import com.wolfssl.wolfcrypt.WolfCryptError; import com.wolfssl.wolfcrypt.WolfCryptException; /** * wolfCrypt JCE Cipher (AES, 3DES) wrapper */ public class WolfCryptCipher extends CipherSpi { enum CipherType { WC_AES, WC_DES3, WC_RSA } enum CipherMode { WC_ECB, WC_CBC, WC_CTR, WC_OFB, WC_GCM, WC_CCM, WC_CTS } enum PaddingType { WC_NONE, WC_PKCS1, WC_PKCS5, WC_OAEP_SHA1, WC_OAEP_SHA256 } enum OpMode { WC_ENCRYPT, WC_DECRYPT } enum RsaKeyType { WC_RSA_PRIVATE, WC_RSA_PUBLIC } private CipherType cipherType = null; private CipherMode cipherMode = null; private PaddingType paddingType = null; private OpMode direction = null; private RsaKeyType rsaKeyType = null; /* Store original opmode (ENCRYPT, DECRYPT, WRAP, UNWRAP), used by * post-doFinal reset to restore correct mode */ private int storedOpMode = 0; private int blockSize = 0; private Aes aes = null; private AesEcb aesEcb = null; private AesCtr aesCtr = null; private AesOfb aesOfb = null; private AesGcm aesGcm = null; private AesCcm aesCcm = null; private AesCts aesCts = null; private Des3 des3 = null; private Rsa rsa = null; private Rng rng = null; /* RSA-OAEP parameters */ private int oaepHashType = 0; private int oaepMgf = 0; /* for debug logging */ private String algString; private String algMode; /* stash key and IV here for easy lookup */ private Key storedKey = null; private AlgorithmParameterSpec storedSpec = null; private byte[] iv = null; /* AES-GCM/CCM tag length (bytes), default to 128 bits */ private int gcmTagLen = 16; /* AAD data for AES-GCM/CCM, accumulated via engineUpdateAAD() */ private ByteArrayOutputStream aadStream = null; /* Last (key, IV) set for AES-GCM encryption at init time, tracked to * reject GCM nonce reuse on re-initialization. A digest of the encoded * key (SHA-512, or SHA-256 when SHA-512 is not compiled in) is stored * instead of the key bytes to avoid keeping an extra copy of key * material in memory. */ private byte[] lastGcmEncryptKeyHash = null; private byte[] lastGcmEncryptIv = null; /* Set when an AES-GCM encryption completes, cleared by init. A second * encryption without re-init would reuse the same key and IV */ private boolean gcmEncryptNeedsReinit = false; /* Has update/final been called yet, gates setting of AAD for GCM */ private boolean operationStarted = false; /* Has this Cipher been inintialized? */ private boolean cipherInitialized = false; /* Buffered data from update calls, only the first bufferedLen bytes * are valid. Capacity grows to max(2 * capacity, needed). */ private byte[] buffered = new byte[0]; private int bufferedLen = 0; /* Max buffered size. Below Integer.MAX_VALUE, which is not allocatable * in full because VMs reserve array header words. */ private static final int MAX_BUFFERED_SIZE = Integer.MAX_VALUE - 8; /* Capacity above this is released on reset instead of kept, so one * large message does not pin memory for the life of the Cipher. */ private static final int MAX_RETAINED_SIZE = 64 * 1024; /** * Append len bytes of src to the buffered data. * * Any buffer replaced during growth is zeroized before being dropped. * * @param src array to append from * @param offset offset into src to start at * @param len number of bytes to append * * @throws IllegalArgumentException if the total buffered size would * exceed MAX_BUFFERED_SIZE */ private void bufferedAppend(byte[] src, int offset, int len) { int newCap, needed; if (len > (MAX_BUFFERED_SIZE - this.bufferedLen)) { throw new IllegalArgumentException( "Buffered input would exceed maximum size of " + MAX_BUFFERED_SIZE + " bytes"); } needed = this.bufferedLen + len; if (needed > this.buffered.length) { newCap = this.buffered.length * 2; if (newCap < 0 || newCap > MAX_BUFFERED_SIZE) { /* Doubling overflowed or passed the cap, pin to max */ newCap = MAX_BUFFERED_SIZE; } if (newCap < needed) { /* Single append larger than double, cap at needed */ newCap = needed; } byte[] tmp = new byte[newCap]; System.arraycopy(this.buffered, 0, tmp, 0, this.bufferedLen); Arrays.fill(this.buffered, 0, this.bufferedLen, (byte)0); this.buffered = tmp; } System.arraycopy(src, offset, this.buffered, this.bufferedLen, len); this.bufferedLen = needed; } /** * Drop and zeroize all buffered data. * * Bytes at or past bufferedLen are always zero already, so only the * used prefix is cleared. Capacity is kept for reuse unless it is * above MAX_RETAINED_SIZE, which is released instead. */ private void bufferedReset() { Arrays.fill(this.buffered, 0, this.bufferedLen, (byte)0); this.bufferedLen = 0; if (this.buffered.length > MAX_RETAINED_SIZE) { this.buffered = new byte[0]; } } /** * Drop the first count bytes, shifting the remainder to the front. * * @param count bytes to drop, must be between 0 and bufferedLen */ private void bufferedConsume(int count) { int remaining = this.bufferedLen - count; System.arraycopy(this.buffered, count, this.buffered, 0, remaining); Arrays.fill(this.buffered, remaining, this.bufferedLen, (byte)0); this.bufferedLen = remaining; } private WolfCryptCipher(CipherType type, CipherMode mode, PaddingType pad) { this.cipherType = type; this.cipherMode = mode; this.paddingType = pad; /* Initialize OAEP parameters if using OAEP padding */ if (pad == PaddingType.WC_OAEP_SHA256) { initOaepParams(); } else if (pad == PaddingType.WC_OAEP_SHA1) { initOaepParamsSha1(); } switch (cipherType) { case WC_AES: blockSize = Aes.BLOCK_SIZE; break; case WC_DES3: blockSize = Des3.BLOCK_SIZE; break; case WC_RSA: break; } if (WolfCryptDebug.DEBUG) { algString = typeToString(cipherType); algMode = modeToString(cipherMode); } } /** * Initialize OAEP parameters for RSA-OAEP padding. * Uses SHA-256 for OAEP hash and SHA-1 for MGF1 hash to match * JCE default behavior for OAEPWithSHA-256AndMGF1Padding. */ private void initOaepParams() { this.oaepHashType = WolfCrypt.WC_HASH_TYPE_SHA256; this.oaepMgf = Rsa.WC_MGF1SHA1; } /** * Initialize OAEP parameters for RSA-OAEP padding with SHA-1. * Uses SHA-1 for OAEP hash and SHA-1 for MGF1 hash to match * JCE default behavior for OAEPWithSHA-1AndMGF1Padding. */ private void initOaepParamsSha1() { this.oaepHashType = WolfCrypt.WC_HASH_TYPE_SHA; this.oaepMgf = Rsa.WC_MGF1SHA1; } /** * Convert JCE hash algorithm name to wolfCrypt hash type constant. * * @param hashAlgo JCE hash algorithm name (e.g., "SHA-256", "SHA-1") * @return wolfCrypt hash type constant * @throws InvalidAlgorithmParameterException if hash algorithm is not * supported */ private int hashNameToWolfCryptType(String hashAlgo) throws InvalidAlgorithmParameterException { if (hashAlgo == null) { throw new InvalidAlgorithmParameterException( "Hash algorithm name cannot be null"); } switch (hashAlgo.toUpperCase()) { case "SHA-1": case "SHA1": return WolfCrypt.WC_HASH_TYPE_SHA; case "SHA-224": case "SHA224": return WolfCrypt.WC_HASH_TYPE_SHA224; case "SHA-256": case "SHA256": return WolfCrypt.WC_HASH_TYPE_SHA256; case "SHA-384": case "SHA384": return WolfCrypt.WC_HASH_TYPE_SHA384; case "SHA-512": case "SHA512": return WolfCrypt.WC_HASH_TYPE_SHA512; default: throw new InvalidAlgorithmParameterException( "Unsupported OAEP hash algorithm: " + hashAlgo); } } /** * Convert MGF1ParameterSpec to wolfCrypt MGF type constant. * * @param mgfSpec MGF1ParameterSpec containing the hash algorithm * @return wolfCrypt MGF type constant * @throws InvalidAlgorithmParameterException if MGF hash algorithm is not * supported */ private int mgf1SpecToWolfCryptMgf(MGF1ParameterSpec mgfSpec) throws InvalidAlgorithmParameterException { if (mgfSpec == null) { throw new InvalidAlgorithmParameterException( "MGF1ParameterSpec cannot be null"); } String hashAlgo = mgfSpec.getDigestAlgorithm(); switch (hashAlgo.toUpperCase()) { case "SHA-1": case "SHA1": return Rsa.WC_MGF1SHA1; case "SHA-224": case "SHA224": return Rsa.WC_MGF1SHA224; case "SHA-256": case "SHA256": return Rsa.WC_MGF1SHA256; case "SHA-384": case "SHA384": return Rsa.WC_MGF1SHA384; case "SHA-512": case "SHA512": return Rsa.WC_MGF1SHA512; default: throw new InvalidAlgorithmParameterException( "Unsupported MGF1 hash algorithm: " + hashAlgo); } } /** * Set OAEP parameters from OAEPParameterSpec. * * @param spec OAEPParameterSpec containing OAEP parameters * @throws InvalidAlgorithmParameterException if parameters are invalid */ @SuppressWarnings("deprecation") private void setOaepParams(OAEPParameterSpec spec) throws InvalidAlgorithmParameterException { AlgorithmParameterSpec mgfParams = null; PSource pSource = null; if (spec == null) { throw new InvalidAlgorithmParameterException( "OAEPParameterSpec cannot be null"); } /* Validate MGF algorithm is MGF1 */ if (!spec.getMGFAlgorithm().equals("MGF1") && !spec.getMGFAlgorithm().equals(OAEPParameterSpec.DEFAULT. getMGFAlgorithm())) { throw new InvalidAlgorithmParameterException( "Only MGF1 is supported for OAEP, got: " + spec.getMGFAlgorithm()); } /* Get MGF parameters */ mgfParams = spec.getMGFParameters(); if (!(mgfParams instanceof MGF1ParameterSpec)) { throw new InvalidAlgorithmParameterException( "MGF parameters must be MGF1ParameterSpec"); } /* Validate PSource is PSpecified with empty label (default) */ pSource = spec.getPSource(); if (pSource != null && pSource instanceof PSource.PSpecified) { byte[] label = ((PSource.PSpecified) pSource).getValue(); if (label != null && label.length > 0) { throw new InvalidAlgorithmParameterException( "OAEP label (PSource) must be empty, custom labels " + "are not supported"); } } /* Set OAEP hash type */ this.oaepHashType = hashNameToWolfCryptType(spec.getDigestAlgorithm()); /* Set MGF type */ this.oaepMgf = mgf1SpecToWolfCryptMgf((MGF1ParameterSpec) mgfParams); log("set OAEP params: hash=" + spec.getDigestAlgorithm() + ", mgf1Hash=" + ((MGF1ParameterSpec) mgfParams). getDigestAlgorithm()); } /** * Reset / re-create internal native struct for algorithm. * Should be called during wolfCryptInit() and wolfCryptFinal() */ private void InitializeNativeStructs() { switch (this.cipherType) { case WC_AES: if (cipherMode == CipherMode.WC_CBC) { if (aes != null) { aes.releaseNativeStruct(); aes = null; } aes = new Aes(); } else if (cipherMode == CipherMode.WC_ECB) { if (aesEcb != null) { aesEcb.releaseNativeStruct(); aesEcb = null; } aesEcb = new AesEcb(); } else if (cipherMode == CipherMode.WC_CTR) { if (aesCtr != null) { aesCtr.releaseNativeStruct(); aesCtr = null; } aesCtr = new AesCtr(); } else if (cipherMode == CipherMode.WC_OFB) { if (aesOfb != null) { aesOfb.releaseNativeStruct(); aesOfb = null; } aesOfb = new AesOfb(); } else if (cipherMode == CipherMode.WC_GCM) { if (aesGcm != null) { aesGcm.releaseNativeStruct(); aesGcm = null; } aesGcm = new AesGcm(); } else if (cipherMode == CipherMode.WC_CCM) { if (aesCcm != null) { aesCcm.releaseNativeStruct(); aesCcm = null; } aesCcm = new AesCcm(); } else if (cipherMode == CipherMode.WC_CTS) { if (aesCts != null) { aesCts.releaseNativeStruct(); aesCts = null; } aesCts = new AesCts(); } break; case WC_DES3: if (des3 != null) { des3.releaseNativeStruct(); des3 = null; } des3 = new Des3(); break; case WC_RSA: /* RSA struct creation handled in wolfCryptSetKey() */ break; } } @Override protected void engineSetMode(String mode) throws NoSuchAlgorithmException { int supported = 0; if (mode.equals("ECB")) { /* RSA and AES support ECB mode */ if (cipherType == CipherType.WC_RSA || cipherType == CipherType.WC_AES) { cipherMode = CipherMode.WC_ECB; supported = 1; log("set mode to ECB"); } } else if (mode.equals("CBC")) { /* AES and 3DES support CBC */ if (cipherType == CipherType.WC_AES || cipherType == CipherType.WC_DES3 ) { cipherMode = CipherMode.WC_CBC; supported = 1; log("set mode to CBC"); } } else if (mode.equals("CTR")) { /* AES supports CTR */ if (cipherType == CipherType.WC_AES) { cipherMode = CipherMode.WC_CTR; supported = 1; log("set mode to CTR"); } } else if (mode.equals("OFB")) { /* AES supports OFB */ if (cipherType == CipherType.WC_AES) { cipherMode = CipherMode.WC_OFB; supported = 1; log("set mode to OFB"); } } else if (mode.equals("GCM")) { /* AES supports GCM */ if (cipherType == CipherType.WC_AES) { cipherMode = CipherMode.WC_GCM; supported = 1; log("set mode to GCM"); } } else if (mode.equals("CCM")) { /* AES supports CCM */ if (cipherType == CipherType.WC_AES) { cipherMode = CipherMode.WC_CCM; supported = 1; log("set mode to CCM"); } } else if (mode.equals("CTS")) { /* AES supports CTS */ if (cipherType == CipherType.WC_AES) { cipherMode = CipherMode.WC_CTS; supported = 1; log("set mode to CTS"); } } if (supported == 0) { throw new NoSuchAlgorithmException( "Unsupported cipher mode for active algorithm choice: " + mode); } } @Override protected void engineSetPadding(String padding) throws NoSuchPaddingException { int supported = 0; if (padding.equals("NoPadding")) { if (cipherType == CipherType.WC_AES || cipherType == CipherType.WC_DES3) { paddingType = PaddingType.WC_NONE; supported = 1; log("set padding to NoPadding"); } } else if (padding.equals("PKCS1Padding")) { if (cipherType == CipherType.WC_RSA) { paddingType = PaddingType.WC_PKCS1; supported = 1; log("set padding to PKCS1Padding"); } } else if (padding.equals("PKCS5Padding")) { if ((cipherType == CipherType.WC_AES) && (cipherMode == CipherMode.WC_CBC || cipherMode == CipherMode.WC_ECB)) { paddingType = PaddingType.WC_PKCS5; supported = 1; log("set padding to PKCS5Padding"); } } else if (padding.equals("OAEPWithSHA-256AndMGF1Padding") || padding.equals("OAEPWithSHA256AndMGF1Padding")) { if (cipherType == CipherType.WC_RSA) { paddingType = PaddingType.WC_OAEP_SHA256; initOaepParams(); supported = 1; log("set padding to OAEPWithSHA-256AndMGF1Padding"); } } else if (padding.equals("OAEPWithSHA-1AndMGF1Padding") || padding.equals("OAEPWithSHA1AndMGF1Padding")) { if (cipherType == CipherType.WC_RSA) { paddingType = PaddingType.WC_OAEP_SHA1; initOaepParamsSha1(); supported = 1; log("set padding to OAEPWithSHA-1AndMGF1Padding"); } } if (supported == 0) { throw new NoSuchPaddingException( "Unsupported padding type for active algorithm choice: " + padding); } } @Override protected int engineGetBlockSize() { return this.blockSize; } @Override protected int engineGetOutputSize(int inputLen) throws IllegalStateException { int outSize = 0; int totalSz = inputLen; int totalBlocks = 0; if (!this.cipherInitialized) { throw new IllegalStateException( "Cipher has not been initialized yet"); } /* Add buffered data size to input length, calculate total blocks */ if (isBlockCipher()) { if (bufferedLen > 0) { totalSz = inputLen + bufferedLen; } else { totalSz = inputLen; } /* For block ciphers that require block boundaries, round * to next block size. GCM, CCM, CTR, CTS, and OFB do not require * block boundaries. */ if (cipherMode != CipherMode.WC_GCM && cipherMode != CipherMode.WC_CCM && cipherMode != CipherMode.WC_CTR && cipherMode != CipherMode.WC_CTS && cipherMode != CipherMode.WC_OFB) { totalBlocks = totalSz / blockSize; totalSz = totalBlocks * blockSize; } } switch (this.cipherType) { case WC_AES: if (paddingType == PaddingType.WC_NONE) { if (cipherMode == CipherMode.WC_GCM) { /* In AES-GCM mode we append the authentication tag * to the end of ciphertext, When decrypting, output * size will have it taken off. */ if (this.direction == OpMode.WC_ENCRYPT) { outSize = totalSz + this.gcmTagLen; } else { outSize = totalSz - this.gcmTagLen; } outSize = Math.max(outSize, 0); } else { /* wolfCrypt expects input to be padded by application * to block size, thus output is same size as input. * If we have buffered data, and that plus inputLen * makes another block, we will have one more block of * data. */ outSize = totalSz; } } else if (paddingType == PaddingType.WC_PKCS5) { outSize = inputLen; if (bufferedLen > 0) { outSize += bufferedLen; } /* Only add padding size when encrypting. When decrypting, * the output size should not include padding bytes since * they will be stripped off during decryption. */ if (this.direction == OpMode.WC_ENCRYPT) { outSize += Aes.getPKCS7PadSize(outSize, Aes.BLOCK_SIZE); } } else { throw new IllegalStateException( "Unsupported padding mode for Cipher Aes"); } break; case WC_DES3: if (paddingType == PaddingType.WC_NONE) { /* wolfCrypt expects input to be padded by application to * block size, thus output is same size as input */ outSize = totalSz; } else if (paddingType == PaddingType.WC_PKCS5) { outSize = inputLen; if (bufferedLen > 0) { outSize += bufferedLen; } /* Only add padding size when encrypting. When decrypting, * the output size should not include padding bytes since * they will be stripped off during decryption. */ if (this.direction == OpMode.WC_ENCRYPT) { outSize += Des3.getPKCS7PadSize(outSize, Des3.BLOCK_SIZE); } } else { throw new IllegalStateException( "Unsupported padding mode for Cipher Des3"); } break; case WC_RSA: outSize = this.rsa.getEncryptSize(); break; } return outSize; } @Override protected byte[] engineGetIV() { if (this.iv != null) { return this.iv.clone(); } return null; } @Override protected AlgorithmParameters engineGetParameters() { AlgorithmParameters params = null; try { switch (this.cipherMode) { case WC_GCM: case WC_CCM: /* Return parameters only if initialized */ if (this.iv != null && this.gcmTagLen > 0) { params = AlgorithmParameters.getInstance("GCM"); GCMParameterSpec gcmSpec = new GCMParameterSpec( this.gcmTagLen * 8, this.iv); params.init(gcmSpec); } break; case WC_CBC: case WC_CTR: case WC_OFB: if (this.iv != null) { if (this.cipherType == CipherType.WC_AES) { params = AlgorithmParameters.getInstance("AES"); } else if (this.cipherType == CipherType.WC_DES3) { params = AlgorithmParameters.getInstance("DESede"); } if (params != null) { IvParameterSpec ivSpec = new IvParameterSpec(this.iv); params.init(ivSpec); } } break; /* ECB mode doesn't have parameters to return */ case WC_ECB: break; } } catch (NoSuchAlgorithmException | InvalidParameterSpecException e) { /* Return null if parameter creation fails */ params = null; } return params; } private void wolfCryptSetDirection(int opmode) throws InvalidKeyException { this.storedOpMode = opmode; switch (opmode) { case Cipher.ENCRYPT_MODE: this.direction = OpMode.WC_ENCRYPT; break; case Cipher.DECRYPT_MODE: this.direction = OpMode.WC_DECRYPT; break; case Cipher.WRAP_MODE: this.direction = OpMode.WC_ENCRYPT; break; case Cipher.UNWRAP_MODE: this.direction = OpMode.WC_DECRYPT; break; default: throw new InvalidParameterException( "Cipher opmode must be ENCRYPT_MODE, " + "DECRYPT_MODE, WRAP_MODE, or UNWRAP_MODE"); } } /** * Convert an AEAD tag length from bits to bytes. * * Rejects lengths that are not a whole number of bytes, since the byte * count is what reaches native wolfSSL and a non-multiple of 8 would be * silently truncated. Native validates which sizes the mode supports. * * @param modeName mode label used in the error message * @param tagBits requested tag length in bits * * @return tag length in bytes * * @throws InvalidAlgorithmParameterException if tagBits is not a * positive multiple of 8 */ private static int tagLenToBytes(String modeName, int tagBits) throws InvalidAlgorithmParameterException { if (tagBits <= 0 || (tagBits % 8) != 0) { throw new InvalidAlgorithmParameterException( modeName + " tag length must be a positive multiple of " + "8 bits, got " + tagBits); } return (tagBits / 8); } private void wolfCryptSetIV(AlgorithmParameterSpec spec, SecureRandom random) throws InvalidAlgorithmParameterException { /* store AlgorithmParameterSpec for class reset */ this.storedSpec = spec; /* Handle RSA OAEP parameters if provided */ if (this.cipherType == CipherType.WC_RSA) { if (spec != null) { if (spec instanceof OAEPParameterSpec) { if (this.paddingType != PaddingType.WC_OAEP_SHA256 && this.paddingType != PaddingType.WC_OAEP_SHA1) { throw new InvalidAlgorithmParameterException( "OAEPParameterSpec can only be used with " + "OAEP padding modes"); } setOaepParams((OAEPParameterSpec) spec); } else { throw new InvalidAlgorithmParameterException( "AlgorithmParameterSpec for RSA OAEP must be of " + "type OAEPParameterSpec"); } } return; } /* AES-ECB doesn't need an IV */ if (this.cipherType == CipherType.WC_AES && this.cipherMode == CipherMode.WC_ECB) return; /* store IV, or generate random IV if not available */ if (spec == null) { this.iv = new byte[this.blockSize]; if (random != null) { random.nextBytes(this.iv); } else { SecureRandom rand = new SecureRandom(); rand.nextBytes(this.iv); } } else { if (cipherMode == CipherMode.WC_GCM) { if (!(spec instanceof GCMParameterSpec)) { throw new InvalidAlgorithmParameterException( "AlgorithmParameterSpec must be of type " + "GCMParameterSpec"); } GCMParameterSpec gcmSpec = (GCMParameterSpec)spec; if (gcmSpec.getIV() == null || gcmSpec.getIV().length == 0) { throw new InvalidAlgorithmParameterException( "AES-GCM IV is null or 0 length"); } this.iv = gcmSpec.getIV().clone(); this.gcmTagLen = tagLenToBytes("AES-GCM", gcmSpec.getTLen()); } else if (cipherMode == CipherMode.WC_CCM) { /* * CCM Parameter Handling: * We use GCMParameterSpec for CCM mode to maintain * compatibility with: * 1. Java 8+ (CCMParameterSpec only available in Java 11+) * 2. BouncyCastle provider (uses GCMParameterSpec for CCM) * 3. Existing developer expectations and code patterns */ if (!(spec instanceof GCMParameterSpec)) { throw new InvalidAlgorithmParameterException( "AlgorithmParameterSpec must be of type " + "GCMParameterSpec for AES-CCM"); } GCMParameterSpec ccmSpec = (GCMParameterSpec)spec; if (ccmSpec.getIV() == null || ccmSpec.getIV().length == 0) { throw new InvalidAlgorithmParameterException( "AES-CCM nonce is null or 0 length"); } /* CCM nonce length validation (7-15 bytes typical) */ if (ccmSpec.getIV().length < 7 || ccmSpec.getIV().length > 15) { throw new InvalidAlgorithmParameterException( "CCM nonce length must be 7-15 bytes, got: " + ccmSpec.getIV().length); } this.iv = ccmSpec.getIV().clone(); this.gcmTagLen = tagLenToBytes("AES-CCM", ccmSpec.getTLen()); } else { if (!(spec instanceof IvParameterSpec)) { throw new InvalidAlgorithmParameterException( "AlgorithmParameterSpec must be of type " + "IvParameterSpec"); } IvParameterSpec ivSpec = (IvParameterSpec)spec; /* IV should be of block size length */ if (ivSpec.getIV().length != this.blockSize) { throw new InvalidAlgorithmParameterException( "Bad IV length (" + ivSpec.getIV().length + "), must be " + blockSize + " bytes long"); } this.iv = ivSpec.getIV().clone(); } } } private void wolfCryptSetKey(Key key) throws InvalidKeyException { byte[] encodedKey; /* validate key class type */ if (this.cipherType == CipherType.WC_RSA) { if (key instanceof RSAPrivateKey) { this.rsaKeyType = RsaKeyType.WC_RSA_PRIVATE; /* wolfSSL requires CRT parameters for RSA private key * operations. Non-CRT keys (created with only modulus and * private exponent) will fail with "mp_exptmod error state" * or similar in the native layer. */ if (!(key instanceof RSAPrivateCrtKey)) { throw new InvalidKeyException( "wolfSSL requires RSA private keys to include CRT " + "parameters (p, q, dP, dQ, qInv). Keys created from " + "only modulus and exponent are not supported."); } } else if (key instanceof RSAPublicKey) { this.rsaKeyType = RsaKeyType.WC_RSA_PUBLIC; } else { throw new InvalidKeyException( "Cipher key must be of type RSAPrivateKey or " + "RSAPublicKey when used for RSA encrypt or decrypt"); } } else if (!(key instanceof SecretKey)) { throw new InvalidKeyException( "Cipher key must be of type SecretKey"); } /* save key for class state resets */ this.storedKey = key; /* import key */ encodedKey = key.getEncoded(); if (encodedKey == null) { throw new InvalidKeyException("Key does not support encoding"); } try { switch (cipherType) { case WC_AES: if (this.direction == OpMode.WC_ENCRYPT) { if (cipherMode == CipherMode.WC_GCM) { this.aesGcm.setKey(encodedKey); } else if (cipherMode == CipherMode.WC_CCM) { this.aesCcm.setKey(encodedKey); } else if (cipherMode == CipherMode.WC_CTS) { this.aesCts.setKey( encodedKey, iv, AesCts.ENCRYPT_MODE); } else if (cipherMode == CipherMode.WC_ECB) { this.aesEcb.setKey( encodedKey, null, AesEcb.ENCRYPT_MODE); } else if (cipherMode == CipherMode.WC_CTR) { this.aesCtr.setKey(encodedKey, iv); } else if (cipherMode == CipherMode.WC_OFB) { this.aesOfb.setKey( encodedKey, iv, AesOfb.ENCRYPT_MODE); } else { this.aes.setKey(encodedKey, iv, Aes.ENCRYPT_MODE); } } else { if (cipherMode == CipherMode.WC_GCM) { this.aesGcm.setKey(encodedKey); } else if (cipherMode == CipherMode.WC_CCM) { this.aesCcm.setKey(encodedKey); } else if (cipherMode == CipherMode.WC_CTS) { this.aesCts.setKey( encodedKey, iv, AesCts.DECRYPT_MODE); } else if (cipherMode == CipherMode.WC_ECB) { this.aesEcb.setKey( encodedKey, null, AesEcb.DECRYPT_MODE); } else if (cipherMode == CipherMode.WC_CTR) { this.aesCtr.setKey(encodedKey, iv); } else if (cipherMode == CipherMode.WC_OFB) { this.aesOfb.setKey( encodedKey, iv, AesOfb.ENCRYPT_MODE); } else { this.aes.setKey(encodedKey, iv, Aes.DECRYPT_MODE); } } break; case WC_DES3: if (this.direction == OpMode.WC_ENCRYPT) { this.des3.setKey(encodedKey, iv, Des3.ENCRYPT_MODE); } else { this.des3.setKey(encodedKey, iv, Des3.DECRYPT_MODE); } break; case WC_RSA: /* reset key struct if needed */ if (this.rsa != null) this.rsa.releaseNativeStruct(); if (this.rng == null) { this.rng = new Rng(); this.rng.init(); } this.rsa = new Rsa(); this.rsa.setRng(this.rng); if (this.rsaKeyType == RsaKeyType.WC_RSA_PRIVATE) { this.rsa.decodePrivateKeyPKCS8(encodedKey); } else { this.rsa.decodePublicKey(encodedKey); } break; } } finally { zeroArray(encodedKey); } } /** * Reject AES-GCM encrypt init with the same key+IV as the last encrypt * init used (GCM nonce reuse). * * Only the pair from the most recent encrypt init is tracked, so any * intermediate encrypt init (key-only with a fresh random IV, or a * different explicit IV) erases the memory of a previously used pair * and a caller can deliberately reuse an earlier (key, IV) pair. This * is a best-effort guard against accidental reuse, kept for SunJCE * interoperability. Callers that opt out this way own IV uniqueness * themselves. * * Called from wolfCryptCipherInit(). The new pair is recorded by * recordGcmKeyIv() only after wolfCryptSetKey() accepts the key, so * a failed init does not erase the tracked pair. Encrypting again * without re-init is rejected separately via gcmEncryptNeedsReinit. * * @param key key this Cipher is being initialized with * * @throws InvalidAlgorithmParameterException if key and IV match the * previous AES-GCM encrypt initialization */ private void checkGcmKeyIvReuse(Key key) throws InvalidAlgorithmParameterException { byte[] keyEnc = null; if ((this.cipherMode != CipherMode.WC_GCM) || (this.direction != OpMode.WC_ENCRYPT) || (key == null)) { return; } keyEnc = key.getEncoded(); if (keyEnc == null) { /* wolfCryptSetKey() will reject this key */ return; } try { if (this.lastGcmEncryptIv != null && MessageDigest.isEqual(this.lastGcmEncryptIv, this.iv) && MessageDigest.isEqual(this.lastGcmEncryptKeyHash, hashKeyForGcmTracking(keyEnc))) { throw new InvalidAlgorithmParameterException( "Cannot reuse iv for GCM encryption"); } } finally { zeroArray(keyEnc); } } /** * Compute digest of encoded key bytes for GCM (key, IV) reuse * tracking. A digest is stored and compared instead of the raw key * bytes to avoid keeping an extra copy of key material in memory. * * SHA-512 is preferred to keep this compatible with CNSA 2.0 * deployments, which restrict hashing to SHA-384/SHA-512. SHA-256 is * used as fallback when SHA-512 is not compiled into native wolfSSL. * * @param keyEnc encoded key bytes * * @return digest of keyEnc */ private static byte[] hashKeyForGcmTracking(byte[] keyEnc) { if (FeatureDetect.Sha512Enabled()) { Sha512 sha = new Sha512(); try { sha.update(keyEnc); return sha.digest(); } finally { sha.releaseNativeStruct(); } } else { Sha256 sha = new Sha256(); try { sha.update(keyEnc); return sha.digest(); } finally { sha.releaseNativeStruct(); } } } /** * Record the (key, IV) pair used for this AES-GCM encrypt init, * checked against by checkGcmKeyIvReuse() on the next encrypt init. * * Called from wolfCryptCipherInit() after wolfCryptSetKey() has * accepted the key. * * @param key key this Cipher was initialized with */ private void recordGcmKeyIv(Key key) { byte[] keyEnc = null; if ((this.cipherMode != CipherMode.WC_GCM) || (this.direction != OpMode.WC_ENCRYPT) || (key == null)) { return; } keyEnc = key.getEncoded(); if (keyEnc == null) { return; } try { zeroArray(this.lastGcmEncryptKeyHash); this.lastGcmEncryptKeyHash = hashKeyForGcmTracking(keyEnc); } finally { zeroArray(keyEnc); } zeroArray(this.lastGcmEncryptIv); if (this.iv == null) { this.lastGcmEncryptIv = null; } else { this.lastGcmEncryptIv = this.iv.clone(); } } /* called by engineInit() functions */ private void wolfCryptCipherInit(int opmode, Key key, AlgorithmParameterSpec spec, SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException { /* Reset buffered data from any previous operation */ bufferedReset(); InitializeNativeStructs(); wolfCryptSetDirection(opmode); wolfCryptSetIV(spec, random); checkGcmKeyIvReuse(key); wolfCryptSetKey(key); recordGcmKeyIv(key); this.operationStarted = false; this.cipherInitialized = true; this.gcmEncryptNeedsReinit = false; } @Override protected void engineInit(int opmode, Key key, SecureRandom random) throws InvalidKeyException { try { wolfCryptCipherInit(opmode, key, null, random); log("initialized with key"); } catch (InvalidAlgorithmParameterException iape) { throw new InvalidKeyException("Invalid algorithm parameters"); } } @Override protected void engineInit(int opmode, Key key, AlgorithmParameterSpec params, SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException { wolfCryptCipherInit(opmode, key, params, random); log("initialized with key and AlgorithmParameterSpec"); } @Override protected void engineInit(int opmode, Key key, AlgorithmParameters params, SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException { AlgorithmParameterSpec spec = null; try { if (params != null) { if (this.cipherType == CipherType.WC_RSA && (this.paddingType == PaddingType.WC_OAEP_SHA256 || this.paddingType == PaddingType.WC_OAEP_SHA1)) { spec = params.getParameterSpec(OAEPParameterSpec.class); } else if (this.cipherMode == CipherMode.WC_GCM || this.cipherMode == CipherMode.WC_CCM) { spec = params.getParameterSpec(GCMParameterSpec.class); } else { spec = params.getParameterSpec(IvParameterSpec.class); } } log("initialized with key and AlgorithmParameters"); } catch (InvalidParameterSpecException ipe) { throw new InvalidAlgorithmParameterException(ipe); } wolfCryptCipherInit(opmode, key, spec, random); } /* return 1 if this is a block cipher, otherwise 0 */ private boolean isBlockCipher() { boolean isBlockCipher = false; switch (this.cipherType) { case WC_AES: case WC_DES3: isBlockCipher = true; break; default: isBlockCipher = false; break; }; return isBlockCipher; } /** * If a call to update() would be a no-op (or just return byte[0]) for the * selected cipher type, mode, and buffered data, return true. * * This happens in cases like RSA, or AES-GCM/CCM which don't support * streaming and buffer all data until doFinal() is called. * * @param inputSz total size in bytes of data available for processing, * including input data and buffered data. * * @return true if update() would be a no-op, otherwise false */ private boolean isNoOpUpdate(int inputSz) { /* RSA keeps buffered data until final() call */ if (cipherType == CipherType.WC_RSA) { return true; } /* AES-GCM, AES-CCM, and AES-CTS keep all data buffered until * final() call. wolfJCE does not support streaming GCM/CCM yet. * CTS requires the entire message for ciphertext stealing. */ if (cipherType == CipherType.WC_AES && (cipherMode == CipherMode.WC_GCM || cipherMode == CipherMode.WC_CCM || cipherMode == CipherMode.WC_CTS)) { return true; } /* If total data input (plus buffered) is less than block size, * update() is a no-op, except for CTR and OFB which are stream * ciphers */ if ((inputSz < blockSize) && (cipherMode != CipherMode.WC_CTR) && (cipherMode != CipherMode.WC_OFB)) { return true; } return false; } private byte[] wolfCryptUpdate(byte[] input, int inputOffset, int len) throws IllegalArgumentException { int blocks = 0; int bytesToProcess = 0; byte[] output = null; byte[] tmpIn = null; if (input == null || len < 0 || inputOffset < 0) { throw new IllegalArgumentException( "Null input buffer or len/offset < 0"); } if (input.length < (inputOffset + len)) { throw new IllegalArgumentException( "Input buffer length smaller than inputOffset + len"); } /* A second GCM encryption without re-init would reuse the same * key and IV, fail here at update() time like SunJCE does */ if ((this.cipherMode == CipherMode.WC_GCM) && (this.direction == OpMode.WC_ENCRYPT) && this.gcmEncryptNeedsReinit) { throw new IllegalStateException( "Must use either different key or iv for GCM encryption"); } this.operationStarted = true; if ((bufferedLen + len) == 0) { /* no data to process */ return null; } if (len > 0) { /* add input bytes to buffered */ bufferedAppend(input, inputOffset, len); } /* Some algos/modes keep data buffered until the doFinal() call, like * RSA or AES-GCM/CCM without stream mode compiled natively. Just * return an empty byte array in those cases here. */ if (isNoOpUpdate(bufferedLen)) { return new byte[0]; } /* Calculate blocks and partial non-block size remaining */ blocks = bufferedLen / blockSize; bytesToProcess = blocks * blockSize; /* CTR and OFB are stream ciphers, process all available data */ if (cipherMode == CipherMode.WC_CTR || cipherMode == CipherMode.WC_OFB) { bytesToProcess = bufferedLen; } /* If PKCS#5/7 padding, and decrypting, hold on to last block for * padding check in wolfCryptFinal() */ else if (paddingType == PaddingType.WC_PKCS5 && direction == OpMode.WC_DECRYPT && bytesToProcess > 0) { bytesToProcess -= blockSize; } /* Not enough data to process yet return until more or final */ if (bytesToProcess == 0) { return new byte[0]; } tmpIn = new byte[bytesToProcess]; System.arraycopy(buffered, 0, tmpIn, 0, bytesToProcess); /* keep remaining non-block size input buffered */ bufferedConsume(bytesToProcess); /* process tmpIn[] */ switch (this.cipherType) { /* Only CBC/ECB/CTR/OFB mode reaches this point currently, * GCM/CCM/CTS cache all data internally above until final call */ case WC_AES: if (cipherMode == CipherMode.WC_ECB) { output = this.aesEcb.update(tmpIn, 0, tmpIn.length); } else if (cipherMode == CipherMode.WC_CTR) { output = this.aesCtr.update(tmpIn, 0, tmpIn.length); } else if (cipherMode == CipherMode.WC_OFB) { output = this.aesOfb.update(tmpIn, 0, tmpIn.length); } else { output = this.aes.update(tmpIn, 0, tmpIn.length); /* truncate */ output = Arrays.copyOfRange(output, 0, tmpIn.length); } break; case WC_DES3: output = this.des3.update(tmpIn, 0, tmpIn.length); /* truncate */ output = Arrays.copyOfRange(output, 0, tmpIn.length); break; default: throw new RuntimeException("Unsupported algorithm type"); }; if (output == null) { /* For interop compatibility, return empty byte array */ output = new byte[0]; } return output; } /** * Normalize an RSA ciphertext byte array to the modulus byte length * before passing it to native wc_RsaPrivateDecrypt / wc_RsaSSL_Verify. * * Proper PKCS#1 v1.5 and OAEP ciphertexts are exactly k bytes long * (modulus length). In practice, zero bytes could be stripped. This * left-pads input with zeros, leaving inputs longer than the modulus * as error cases. * * @param in ciphertext bytes * @param modSize RSA modulus length in bytes * @return a byte[] of length exactly modSize (the same array if already * the right size, otherwise a new zero-padded copy) * @throws IllegalBlockSizeException if in.length greater than modSize */ private static byte[] leftPadRSACiphertext(byte[] in, int modSize) throws IllegalBlockSizeException { if (in.length == modSize) { return in; } if (in.length > modSize) { throw new IllegalBlockSizeException("RSA input length (" + in.length + ") exceeds key size (" + modSize + ")"); } byte[] padded = new byte[modSize]; System.arraycopy(in, 0, padded, modSize - in.length, in.length); return padded; } private byte[] wolfCryptFinal(byte[] input, int inputOffset, int len) throws IllegalBlockSizeException, BadPaddingException { int totalSz = 0; byte tmpIn[] = null; byte tmpOut[] = null; this.operationStarted = true; totalSz = bufferedLen + len; /* AES-CTS requires input length >= 16 bytes (RFC 3962/8009). * For exactly 16 bytes, CTS reduces to plain CBC, handled in JNI. */ if (cipherMode == CipherMode.WC_CTS && totalSz < blockSize) { throw new IllegalBlockSizeException( "AES-CTS requires input length >= " + blockSize + " bytes, got " + totalSz + " bytes"); } /* AES-GCM, AES-CCM, AES-CTR, AES-CTS, and AES-OFB do not require * block size inputs */ if (isBlockCipher() && (cipherMode != CipherMode.WC_GCM) && (cipherMode != CipherMode.WC_CCM) && (cipherMode != CipherMode.WC_CTR) && (cipherMode != CipherMode.WC_CTS) && (cipherMode != CipherMode.WC_OFB) && (this.direction == OpMode.WC_DECRYPT || (this.direction == OpMode.WC_ENCRYPT && this.paddingType != PaddingType.WC_PKCS5)) && (totalSz % blockSize != 0)) { throw new IllegalBlockSizeException( "Input length (" + totalSz + ") not multiple of " + blockSize + " bytes. (" + bufferedLen +" buffered)"); } /* do final encrypt over totalSz */ tmpIn = new byte[totalSz]; if (totalSz > 0) { System.arraycopy(buffered, 0, tmpIn, 0, bufferedLen); if (input != null && len > 0) { System.arraycopy(input, inputOffset, tmpIn, bufferedLen, len); } } /* add padding if encrypting and PKCS5 padding is used. PKCS#5 padding * is treated the same as PKCS#7 padding here, using each algorithm's * specific block size. CCM, CTR, CTS, and OFB modes do not use * padding */ if (this.direction == OpMode.WC_ENCRYPT && this.paddingType == PaddingType.WC_PKCS5 && cipherMode != CipherMode.WC_CCM && cipherMode != CipherMode.WC_CTR && cipherMode != CipherMode.WC_CTS && cipherMode != CipherMode.WC_OFB) { if (this.cipherType == CipherType.WC_AES) { tmpIn = Aes.padPKCS7(tmpIn, Aes.BLOCK_SIZE); } else if (this.cipherType == CipherType.WC_DES3) { tmpIn = Des3.padPKCS7(tmpIn, Des3.BLOCK_SIZE); } } /* Flatten accumulated AAD to a single array for GCM/CCM calls below */ byte[] aad = (this.aadStream != null) ? this.aadStream.toByteArray() : null; switch (this.cipherType) { case WC_AES: if (cipherMode == CipherMode.WC_GCM) { if (this.direction == OpMode.WC_ENCRYPT) { /* A second encryption without re-init would reuse * the same key and IV (GCM nonce reuse) */ if (this.gcmEncryptNeedsReinit) { throw new IllegalStateException( "Must use either different key or iv for " + "GCM encryption"); } byte[] tag = new byte[this.gcmTagLen]; tmpOut = this.aesGcm.encrypt(tmpIn, this.iv, tag, aad); this.gcmEncryptNeedsReinit = true; /* Concatenate auth tag to end of ciphertext */ byte[] totalOut = new byte[tmpOut.length + tag.length]; System.arraycopy(tmpOut, 0, totalOut, 0, tmpOut.length); System.arraycopy(tag, 0, totalOut, tmpOut.length, tag.length); tmpOut = totalOut; } else { /* Case where input is only the authentication tag, * zero-length plaintext */ if (tmpIn.length < this.gcmTagLen) { throw new AEADBadTagException( "Input too short for GCM tag, got " + tmpIn.length + " bytes, need at least " + this.gcmTagLen); } /* Get auth tag from end of ciphertext */ byte[] tag = Arrays.copyOfRange(tmpIn, tmpIn.length - this.gcmTagLen, tmpIn.length); /* Shrink ciphertext array down to not include tag */ tmpIn = Arrays.copyOfRange(tmpIn, 0, tmpIn.length - this.gcmTagLen); try { tmpOut = this.aesGcm.decrypt(tmpIn, this.iv, tag, aad); } catch (WolfCryptException e) { /* Convert to AEADBadTagException */ if (e.getCode() == WolfCryptError.AES_GCM_AUTH_E.getCode()) { /* Authentication check fail */ throw new AEADBadTagException(e.getMessage()); } throw e; } } } else if (cipherMode == CipherMode.WC_CCM) { if (this.direction == OpMode.WC_ENCRYPT) { byte[] tag = new byte[this.gcmTagLen]; tmpOut = this.aesCcm.encrypt(tmpIn, this.iv, tag, aad); /* Concatenate auth tag to end of ciphertext */ byte[] totalOut = new byte[tmpOut.length + tag.length]; System.arraycopy(tmpOut, 0, totalOut, 0, tmpOut.length); System.arraycopy(tag, 0, totalOut, tmpOut.length, tag.length); tmpOut = totalOut; } else { /* Case where input is only the authentication tag, * zero-length plaintext */ if (tmpIn.length < this.gcmTagLen) { throw new AEADBadTagException( "Input too short for CCM tag, got " + tmpIn.length + " bytes, need at least " + this.gcmTagLen); } /* Get auth tag from end of ciphertext */ byte[] tag = Arrays.copyOfRange(tmpIn, tmpIn.length - this.gcmTagLen, tmpIn.length); /* Shrink ciphertext array down to not include tag */ tmpIn = Arrays.copyOfRange(tmpIn, 0, tmpIn.length - this.gcmTagLen); tmpOut = this.aesCcm.decrypt(tmpIn, this.iv, tag, aad); } } else if (cipherMode == CipherMode.WC_ECB) { tmpOut = this.aesEcb.update(tmpIn, 0, tmpIn.length); } else if (cipherMode == CipherMode.WC_CTR) { tmpOut = this.aesCtr.update(tmpIn, 0, tmpIn.length); } else if (cipherMode == CipherMode.WC_CTS) { tmpOut = this.aesCts.update(tmpIn, 0, tmpIn.length); } else if (cipherMode == CipherMode.WC_OFB) { tmpOut = this.aesOfb.update(tmpIn, 0, tmpIn.length); } else { tmpOut = this.aes.update(tmpIn, 0, tmpIn.length); /* truncate */ tmpOut = Arrays.copyOfRange(tmpOut, 0, tmpIn.length); } /* strip PKCS#5/PKCS#7 padding if required, * CCM, CTR, CTS, and OFB modes do not use padding */ if (tmpOut != null && tmpOut.length > 0) { if (this.direction == OpMode.WC_DECRYPT && this.paddingType == PaddingType.WC_PKCS5 && cipherMode != CipherMode.WC_CCM && cipherMode != CipherMode.WC_CTR && cipherMode != CipherMode.WC_CTS && cipherMode != CipherMode.WC_OFB) { try { tmpOut = Aes.unPadPKCS7(tmpOut, Aes.BLOCK_SIZE); } catch (WolfCryptException e) { throw new BadPaddingException("Decryption error"); } } } break; case WC_DES3: tmpOut = this.des3.update(tmpIn, 0, tmpIn.length); /* truncate */ tmpOut = Arrays.copyOfRange(tmpOut, 0, tmpIn.length); /* strip PKCS#5/PKCS#7 padding if required */ if (tmpOut != null && tmpOut.length > 0) { if (this.direction == OpMode.WC_DECRYPT && this.paddingType == PaddingType.WC_PKCS5) { try { tmpOut = Des3.unPadPKCS7(tmpOut, Des3.BLOCK_SIZE); } catch (WolfCryptException e) { throw new BadPaddingException("Decryption error"); } } } break; case WC_RSA: if (this.paddingType == PaddingType.WC_OAEP_SHA256 || this.paddingType == PaddingType.WC_OAEP_SHA1) { /* OAEP only supports public key encrypt, private decrypt */ if (this.direction == OpMode.WC_ENCRYPT) { if (this.rsaKeyType == RsaKeyType.WC_RSA_PRIVATE) { throw new IllegalStateException( "OAEP padding requires public key for " + "encryption"); } tmpOut = this.rsa.encryptOaep(tmpIn, this.rng, this.oaepHashType, this.oaepMgf); } else { if (this.rsaKeyType == RsaKeyType.WC_RSA_PUBLIC) { throw new IllegalStateException( "OAEP padding requires private key for " + "decryption"); } try { tmpIn = leftPadRSACiphertext(tmpIn, this.rsa.getEncryptSize()); tmpOut = this.rsa.decryptOaep(tmpIn, this.oaepHashType, this.oaepMgf); } catch (WolfCryptException e) { throw new BadPaddingException("Decryption error"); } } } else { /* PKCS#1 v1.5 padding */ if (this.direction == OpMode.WC_ENCRYPT) { if (this.rsaKeyType == RsaKeyType.WC_RSA_PRIVATE) { tmpOut = this.rsa.sign(tmpIn, this.rng); } else { tmpOut = this.rsa.encrypt(tmpIn, this.rng); } } else { try { tmpIn = leftPadRSACiphertext(tmpIn, this.rsa.getEncryptSize()); if (this.rsaKeyType == RsaKeyType.WC_RSA_PRIVATE) { tmpOut = this.rsa.decrypt(tmpIn); } else { tmpOut = this.rsa.verify(tmpIn); } } catch (WolfCryptException e) { throw new BadPaddingException("Decryption error"); } } } break; default: throw new RuntimeException("Unsupported algorithm type"); }; /* reset state, user doesn't need to call init again before use */ try { bufferedReset(); wolfCryptSetDirection(this.storedOpMode); InitializeNativeStructs(); /* Preserve the existing IV during cipher reset to maintain * consistency with JCE getIV() behavior. If storedSpec is null * (no IV was provided initially), wolfCryptSetIV would generate * a new random IV, overwriting the original one. */ if (storedSpec == null && this.iv != null) { /* Create appropriate ParameterSpec with the current IV to avoid * generating a new random IV during reset */ AlgorithmParameterSpec currentIvSpec; if (cipherMode == CipherMode.WC_GCM) { /* For GCM mode, create GCMParameterSpec with current * IV and tag length */ currentIvSpec = new GCMParameterSpec( this.gcmTagLen * 8, this.iv.clone()); } else { /* For other modes, use IvParameterSpec */ currentIvSpec = new IvParameterSpec(this.iv.clone()); } wolfCryptSetIV(currentIvSpec, null); } else { wolfCryptSetIV(storedSpec, null); } wolfCryptSetKey(storedKey); this.aadStream = null; this.operationStarted = false; this.cipherInitialized = true; } catch (InvalidKeyException e) { throw new RuntimeException(e.getMessage()); } catch (InvalidAlgorithmParameterException e) { throw new RuntimeException(e.getMessage()); } return tmpOut; } @Override protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) throws IllegalStateException { byte output[]; if (!this.cipherInitialized) { throw new IllegalStateException( "Cipher has not been initialized yet"); } log("update (offset: " + inputOffset + ", len: " + inputLen + ")"); output = wolfCryptUpdate(input, inputOffset, inputLen); return output; } /** * Sanity check output buffer size is large enough for update() call, * based on padding and buffered data. * * @param inputSz size of input data to update() * @param outputSz total size of output buffer provided * * @throws ShortBufferException if output buffer is too small */ private void checkUpdateOutputBufferSize(int inputSz, int outputSz) throws ShortBufferException { int outSize; if (!isNoOpUpdate(inputSz)) { outSize = engineGetOutputSize(inputSz); /* update() in DECRYPT mode with PKCS5 padding will hold * back one block of data for padding check in final() */ if (direction == OpMode.WC_DECRYPT && paddingType == PaddingType.WC_PKCS5) { if (outSize % blockSize == 0) { outSize -= blockSize; } else { outSize -= (outSize % blockSize); } } if (outputSz < outSize) { throw new ShortBufferException( "Output buffer too small, need " + outSize + " bytes, got " + outputSz); } } } @Override protected int engineUpdate(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) throws IllegalStateException, ShortBufferException { byte tmpOut[]; if (!this.cipherInitialized) { throw new IllegalStateException( "Cipher has not been initialized yet"); } log("update (inputOffset: " + inputOffset + ", inputLen: " + inputLen + ", outputOffset: " + outputOffset + ")"); if (output == null || (output.length < outputOffset)) { throw new IllegalArgumentException( "output is null or offset past output array sz"); } if (input == null || (inputLen + inputOffset > input.length)) { throw new IllegalArgumentException( "input is null or inOffset + inputLen past input array size"); } /* Sanitize output buffer size, throws ShortBufferException if needed */ checkUpdateOutputBufferSize(inputLen, output.length - outputOffset); tmpOut = wolfCryptUpdate(input, inputOffset, inputLen); if (tmpOut == null) { return 0; } if (output.length - outputOffset < tmpOut.length) { throw new ShortBufferException( "Output buffer too small, need " + tmpOut.length + " bytes, got " + (output.length - outputOffset)); } System.arraycopy(tmpOut, 0, output, outputOffset, tmpOut.length); return tmpOut.length; } private void zeroArray(byte[] in) { if (in == null) return; for (int i = 0; i < in.length; i++) { in[i] = 0; } } @Override protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) throws IllegalStateException, IllegalBlockSizeException, BadPaddingException { if (!this.cipherInitialized) { throw new IllegalStateException( "Cipher has not been initialized yet"); } log("final (offset: " + inputOffset + ", len: " + inputLen + ", buffered: " + bufferedLen + ")"); return wolfCryptFinal(input, inputOffset, inputLen); } @Override protected int engineDoFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) throws IllegalStateException, ShortBufferException, IllegalBlockSizeException, BadPaddingException { byte tmpOut[]; if (!this.cipherInitialized) { throw new IllegalStateException( "Cipher has not been initialized yet"); } log("final (inputOffset: " + inputOffset + ", inputLen: " + inputLen + ", outputOffset: " + outputOffset + ", buffered: " + bufferedLen + ")"); if (output == null || (outputOffset > output.length)) { throw new IllegalArgumentException( "output is null or offset past output array sz"); } /* SunJCE can save Cipher state so it can throw a more precise * ShortBufferException after checking the actual length after * stripping padding. But, native wolfCrypt does not support * saving/restoring Aes state, so we err on the side of making callers * give us up to the next block size of output space */ if ((output.length - outputOffset) < engineGetOutputSize(inputLen)) { throw new ShortBufferException("Output buffer too small, need " + engineGetOutputSize(inputLen) + " bytes, got " + (output.length - outputOffset)); } tmpOut = wolfCryptFinal(input, inputOffset, inputLen); if (output.length - outputOffset < tmpOut.length) { throw new ShortBufferException( "Output buffer too small, need " + tmpOut.length + " bytes, got " + (output.length - outputOffset)); } System.arraycopy(tmpOut, 0, output, outputOffset, tmpOut.length); return tmpOut.length; } @Override protected int engineGetKeySize(Key key) throws InvalidKeyException { byte encodedKey[] = null; int kLen = 0; /* validate key class type */ if (this.cipherType == CipherType.WC_RSA) { if (key instanceof RSAPrivateKey) { this.rsaKeyType = RsaKeyType.WC_RSA_PRIVATE; } else if (key instanceof RSAPublicKey) { this.rsaKeyType = RsaKeyType.WC_RSA_PUBLIC; } else { throw new InvalidKeyException( "Cipher key must be of type RSAPrivateKey or " + "RSAPublicKey when used for RSA encrypt or decrypt"); } } else if (!(key instanceof SecretKey)) { throw new InvalidKeyException( "Cipher key must be of type SecretKey"); } encodedKey = key.getEncoded(); if (encodedKey == null) throw new InvalidKeyException("Key does not support encoding"); kLen = encodedKey.length; zeroArray(encodedKey); return kLen; } @Override protected void engineUpdateAAD(byte[] src, int offset, int len) throws IllegalArgumentException, IllegalStateException { if (this.cipherType != CipherType.WC_AES || (this.cipherMode != CipherMode.WC_GCM && this.cipherMode != CipherMode.WC_CCM)) { throw new IllegalStateException( "AAD only supported for AES-GCM and AES-CCM"); } if (this.operationStarted) { throw new IllegalStateException( "Must set AAD before calling Cipher.update/final"); } if (!this.cipherInitialized || (this.cipherMode == CipherMode.WC_GCM && this.aesGcm == null) || (this.cipherMode == CipherMode.WC_CCM && this.aesCcm == null)) { throw new IllegalStateException( "Cipher not initialized yet"); } if (src == null || offset < 0 || len < 0 || (src.length < (offset + len))) { throw new IllegalArgumentException( "Source buffer is null or bad offset/len"); } if (this.aadStream == null) { this.aadStream = new ByteArrayOutputStream(); } this.aadStream.write(src, offset, len); } @Override protected void engineUpdateAAD(ByteBuffer src) throws IllegalArgumentException, IllegalStateException { int originalPos = 0; byte[] remaining = null; if (src == null) { throw new IllegalArgumentException("Source buffer is null"); } originalPos = src.position(); remaining = new byte[src.remaining()]; src.get(remaining); try { engineUpdateAAD(remaining, 0, remaining.length); } catch (IllegalStateException | IllegalArgumentException e) { /* restore state of ByteBuffer on state error before returning */ src.position(originalPos); throw e; } } @Override protected byte[] engineWrap(Key key) throws IllegalBlockSizeException, InvalidKeyException { byte[] encodedKey = null; byte[] wcBuf; if (key == null) { throw new InvalidKeyException( "Key to be wrapped must not be null"); } encodedKey = key.getEncoded(); if (encodedKey == null) { throw new InvalidKeyException( "Key does not support encoding, cannot wrap"); } try { wcBuf = wolfCryptFinal(encodedKey, 0, encodedKey.length); } catch (BadPaddingException e) { throw new InvalidKeyException("Failed to wrap key: " + e.getMessage(), e); } finally { zeroArray(encodedKey); } return wcBuf; } @Override protected Key engineUnwrap(byte[] wrappedKey, String wrappedKeyAlgo, int wrappedKeyType) throws InvalidKeyException, NoSuchAlgorithmException { byte[] unwrappedKey; if (wrappedKey == null || wrappedKey.length == 0) { throw new InvalidKeyException( "Wrapped key bytes must not be null or empty"); } try { unwrappedKey = wolfCryptFinal(wrappedKey, 0, wrappedKey.length); } catch (BadPaddingException | IllegalBlockSizeException e) { throw new InvalidKeyException("Failed to unwrap key"); } switch (wrappedKeyType) { case Cipher.SECRET_KEY: return new SecretKeySpec(unwrappedKey, wrappedKeyAlgo); case Cipher.PUBLIC_KEY: try { KeyFactory kf = KeyFactory.getInstance(wrappedKeyAlgo); return kf.generatePublic( new X509EncodedKeySpec(unwrappedKey)); } catch (InvalidKeySpecException e) { throw new InvalidKeyException( "Failed to reconstruct public key: " + e.getMessage(), e); } case Cipher.PRIVATE_KEY: try { KeyFactory kf = KeyFactory.getInstance(wrappedKeyAlgo); return kf.generatePrivate( new PKCS8EncodedKeySpec(unwrappedKey)); } catch (InvalidKeySpecException e) { throw new InvalidKeyException( "Failed to reconstruct private key: " + e.getMessage(), e); } default: throw new InvalidKeyException("Invalid wrappedKeyType: " + wrappedKeyType); } } private String typeToString(CipherType type) { switch (type) { case WC_AES: return "AES"; case WC_DES3: return "3DES"; case WC_RSA: return "RSA"; default: return "None"; } } private String modeToString(CipherMode type) { switch (type) { case WC_ECB: return "ECB"; case WC_CBC: return "CBC"; case WC_GCM: return "GCM"; case WC_CCM: return "CCM"; default: return "None"; } } private void log(String msg) { WolfCryptDebug.log(getClass(), WolfCryptDebug.INFO, () -> "[" + algString + "-" + algMode + "] " + msg); } @SuppressWarnings({"deprecation", "removal"}) @Override protected void finalize() throws Throwable { try { if (this.aes != null) { this.aes.releaseNativeStruct(); this.aes = null; } if (this.aesEcb != null) { this.aesEcb.releaseNativeStruct(); this.aesEcb = null; } if (this.aesCtr != null) { this.aesCtr.releaseNativeStruct(); this.aesCtr = null; } if (this.aesOfb != null) { this.aesOfb.releaseNativeStruct(); this.aesOfb = null; } if (this.aesGcm != null) { this.aesGcm.releaseNativeStruct(); this.aesGcm = null; } if (this.aesCcm != null) { this.aesCcm.releaseNativeStruct(); this.aesCcm = null; } if (this.aesCts != null) { this.aesCts.releaseNativeStruct(); this.aesCts = null; } if (this.des3 != null) { this.des3.releaseNativeStruct(); this.des3 = null; } if (this.rsa != null) { this.rsa.releaseNativeStruct(); this.rsa = null; } if (this.rng != null) { this.rng.free(); this.rng.releaseNativeStruct(); this.rng = null; } zeroArray(this.iv); zeroArray(this.lastGcmEncryptKeyHash); zeroArray(this.lastGcmEncryptIv); this.storedKey = null; this.storedSpec = null; } finally { super.finalize(); } } /** * Class for AES-CBC with no padding */ public static final class wcAESCBCNoPadding extends WolfCryptCipher { /** * Create new wcAESCBCNoPadding object */ public wcAESCBCNoPadding() { super(CipherType.WC_AES, CipherMode.WC_CBC, PaddingType.WC_NONE); } } /** * Class for AES-CBC with PKCS#5 padding */ public static final class wcAESCBCPKCS5Padding extends WolfCryptCipher { /** * Create new wcAESCBCPkcs5Padding object */ public wcAESCBCPKCS5Padding() { super(CipherType.WC_AES, CipherMode.WC_CBC, PaddingType.WC_PKCS5); } } /** * Class for AES-GCM with no padding */ public static final class wcAESGCMNoPadding extends WolfCryptCipher { /** * Create new wcAESGCMNoPadding object */ public wcAESGCMNoPadding() { super(CipherType.WC_AES, CipherMode.WC_GCM, PaddingType.WC_NONE); } } /** * Class for AES-CCM with no padding */ public static final class wcAESCCMNoPadding extends WolfCryptCipher { /** * Create new wcAESCCMNoPadding object */ public wcAESCCMNoPadding() { super(CipherType.WC_AES, CipherMode.WC_CCM, PaddingType.WC_NONE); } } /** * Class for DES-EDE-CBC with no padding */ public static final class wcDESedeCBCNoPadding extends WolfCryptCipher { /** * Create new wcDESedeCBCNoPadding object */ public wcDESedeCBCNoPadding() { super(CipherType.WC_DES3, CipherMode.WC_CBC, PaddingType.WC_NONE); } } /** * Class for AES-ECB with no padding */ public static final class wcAESECBNoPadding extends WolfCryptCipher { /** * Create new wcAESECBNoPadding object */ public wcAESECBNoPadding() { super(CipherType.WC_AES, CipherMode.WC_ECB, PaddingType.WC_NONE); } } /** * Class for AES-ECB with PKCS#5 padding */ public static final class wcAESECBPKCS5Padding extends WolfCryptCipher { /** * Create new wcAESECBPKCS5Padding object */ public wcAESECBPKCS5Padding() { super(CipherType.WC_AES, CipherMode.WC_ECB, PaddingType.WC_PKCS5); } } /** * Class for AES-CTR with no padding */ public static final class wcAESCTRNoPadding extends WolfCryptCipher { /** * Create new wcAESCTRNoPadding object */ public wcAESCTRNoPadding() { super(CipherType.WC_AES, CipherMode.WC_CTR, PaddingType.WC_NONE); } } /** * Class for AES-OFB with no padding */ public static final class wcAESOFBNoPadding extends WolfCryptCipher { /** * Create new wcAESOFBNoPadding object */ public wcAESOFBNoPadding() { super(CipherType.WC_AES, CipherMode.WC_OFB, PaddingType.WC_NONE); } } /** * Class for AES-CTS with no padding */ public static final class wcAESCTSNoPadding extends WolfCryptCipher { /** * Create new wcAESCTSNoPadding object */ public wcAESCTSNoPadding() { super(CipherType.WC_AES, CipherMode.WC_CTS, PaddingType.WC_NONE); } } /** * Class for RSA-ECB with PKCS1 padding */ public static final class wcRSAECBPKCS1Padding extends WolfCryptCipher { /** * Create new wcRSAECBPKCS1Padding object */ public wcRSAECBPKCS1Padding() { super(CipherType.WC_RSA, CipherMode.WC_ECB, PaddingType.WC_PKCS1); } } /** * Class for RSA-ECB with OAEP SHA-256 padding */ public static final class wcRSAECBOAEPSHA256Padding extends WolfCryptCipher { /** * Create new wcRSAECBOAEPSHA256Padding object */ public wcRSAECBOAEPSHA256Padding() { super(CipherType.WC_RSA, CipherMode.WC_ECB, PaddingType.WC_OAEP_SHA256); } } /** * Class for RSA-ECB with OAEP SHA-1 padding */ public static final class wcRSAECBOAEPSHA1Padding extends WolfCryptCipher { /** * Create new wcRSAECBOAEPSHA1Padding object */ public wcRSAECBOAEPSHA1Padding() { super(CipherType.WC_RSA, CipherMode.WC_ECB, PaddingType.WC_OAEP_SHA1); } } }