| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.commons.crypto.jna; |
| |
| import java.nio.ByteBuffer; |
| import java.security.GeneralSecurityException; |
| import java.security.InvalidAlgorithmParameterException; |
| import java.security.InvalidKeyException; |
| import java.security.Key; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.spec.AlgorithmParameterSpec; |
| import java.util.Objects; |
| import java.util.Properties; |
| import java.util.StringTokenizer; |
| |
| import javax.crypto.BadPaddingException; |
| import javax.crypto.Cipher; |
| import javax.crypto.IllegalBlockSizeException; |
| import javax.crypto.NoSuchPaddingException; |
| import javax.crypto.ShortBufferException; |
| import javax.crypto.spec.IvParameterSpec; |
| |
| import org.apache.commons.crypto.cipher.CryptoCipher; |
| import org.apache.commons.crypto.cipher.CryptoCipherFactory; |
| |
| import com.sun.jna.NativeLong; |
| import com.sun.jna.ptr.PointerByReference; |
| |
| /** |
| * Implements the CryptoCipher using JNA into OpenSSL. |
| */ |
| class OpenSslJnaCipher implements CryptoCipher { |
| |
| private PointerByReference algo; |
| private final PointerByReference context; |
| private final AlgorithmMode algMode; |
| private final int padding; |
| private final String transformation; |
| private final int IV_LENGTH = 16; |
| |
| /** |
| * Constructs a {@link CryptoCipher} using JNA into OpenSSL |
| * |
| * @param props properties for OpenSSL cipher |
| * @param transformation transformation for OpenSSL cipher |
| * @throws GeneralSecurityException if OpenSSL cipher initialize failed |
| */ |
| public OpenSslJnaCipher(final Properties props, final String transformation) // NOPMD |
| throws GeneralSecurityException { |
| if (!OpenSslJna.isEnabled()) { |
| throw new GeneralSecurityException("Could not enable JNA access", OpenSslJna.initialisationError()); |
| } |
| this.transformation = transformation; |
| final Transform transform = tokenizeTransformation(transformation); |
| algMode = AlgorithmMode.get(transform.algorithm, transform.mode); |
| |
| if (algMode != AlgorithmMode.AES_CBC && algMode != AlgorithmMode.AES_CTR) { |
| throw new GeneralSecurityException("unknown algorithm " + transform.algorithm + "_" + transform.mode); |
| } |
| |
| padding = Padding.get(transform.padding); |
| context = OpenSslNativeJna.EVP_CIPHER_CTX_new(); |
| |
| } |
| |
| /** |
| * Initializes the cipher with mode, key and iv. |
| * |
| * @param mode {@link Cipher#ENCRYPT_MODE} or {@link Cipher#DECRYPT_MODE} |
| * @param key crypto key for the cipher |
| * @param params the algorithm parameters |
| * @throws InvalidKeyException If key length is invalid |
| * @throws InvalidAlgorithmParameterException if IV length is wrong |
| */ |
| @Override |
| public void init(final int mode, final Key key, final AlgorithmParameterSpec params) |
| throws InvalidKeyException, InvalidAlgorithmParameterException { |
| Objects.requireNonNull(key, "key"); |
| Objects.requireNonNull(params, "params"); |
| int cipherMode = OpenSslNativeJna.OOSL_JNA_DECRYPT_MODE; |
| if (mode == Cipher.ENCRYPT_MODE) { |
| cipherMode = OpenSslNativeJna.OOSL_JNA_ENCRYPT_MODE; |
| } |
| final byte[] iv; |
| if (params instanceof IvParameterSpec) { |
| iv = ((IvParameterSpec) params).getIV(); |
| } else { |
| // other AlgorithmParameterSpec such as GCMParameterSpec is not |
| // supported now. |
| throw new InvalidAlgorithmParameterException("Illegal parameters"); |
| } |
| |
| if ((algMode == AlgorithmMode.AES_CBC || algMode == AlgorithmMode.AES_CTR) && iv.length != IV_LENGTH) { |
| throw new InvalidAlgorithmParameterException("Wrong IV length: must be 16 bytes long"); |
| } |
| final int keyEncodedLength = key.getEncoded().length; |
| |
| if (algMode == AlgorithmMode.AES_CBC) { |
| switch (keyEncodedLength) { |
| case 16: |
| algo = OpenSslNativeJna.EVP_aes_128_cbc(); |
| break; |
| case 24: |
| algo = OpenSslNativeJna.EVP_aes_192_cbc(); |
| break; |
| case 32: |
| algo = OpenSslNativeJna.EVP_aes_256_cbc(); |
| break; |
| default: |
| throw new InvalidKeyException("keysize unsupported (" + keyEncodedLength + ")"); |
| } |
| |
| } else { |
| switch (keyEncodedLength) { |
| case 16: |
| algo = OpenSslNativeJna.EVP_aes_128_ctr(); |
| break; |
| case 24: |
| algo = OpenSslNativeJna.EVP_aes_192_ctr(); |
| break; |
| case 32: |
| algo = OpenSslNativeJna.EVP_aes_256_ctr(); |
| break; |
| default: |
| throw new InvalidKeyException("keysize unsupported (" + keyEncodedLength + ")"); |
| } |
| } |
| |
| final int retVal = OpenSslNativeJna.EVP_CipherInit_ex(context, algo, null, key.getEncoded(), iv, cipherMode); |
| throwOnError(retVal); |
| OpenSslNativeJna.EVP_CIPHER_CTX_set_padding(context, padding); |
| } |
| |
| /** |
| * Continues a multiple-part encryption/decryption operation. The data is |
| * encrypted or decrypted, depending on how this cipher was initialized. |
| * |
| * @param inBuffer the input ByteBuffer |
| * @param outBuffer the output ByteBuffer |
| * @return int number of bytes stored in {@code output} |
| * @throws ShortBufferException if there is insufficient space in the output |
| * buffer |
| */ |
| @Override |
| public int update(final ByteBuffer inBuffer, final ByteBuffer outBuffer) throws ShortBufferException { |
| final int[] outlen = new int[1]; |
| final int retVal = OpenSslNativeJna.EVP_CipherUpdate(context, outBuffer, outlen, inBuffer, |
| inBuffer.remaining()); |
| throwOnError(retVal); |
| final int len = outlen[0]; |
| inBuffer.position(inBuffer.limit()); |
| outBuffer.position(outBuffer.position() + len); |
| return len; |
| } |
| |
| /** |
| * Continues a multiple-part encryption/decryption operation. The data is |
| * encrypted or decrypted, depending on how this cipher was initialized. |
| * |
| * @param input the input byte array |
| * @param inputOffset the offset in input where the input starts |
| * @param inputLen the input length |
| * @param output the byte array for the result |
| * @param outputOffset the offset in output where the result is stored |
| * @return the number of bytes stored in output |
| * @throws ShortBufferException if there is insufficient space in the output |
| * byte array |
| */ |
| @Override |
| public int update(final byte[] input, final int inputOffset, final int inputLen, final byte[] output, |
| final int outputOffset) throws ShortBufferException { |
| final ByteBuffer outputBuf = ByteBuffer.wrap(output, outputOffset, output.length - outputOffset); |
| final ByteBuffer inputBuf = ByteBuffer.wrap(input, inputOffset, inputLen); |
| return update(inputBuf, outputBuf); |
| } |
| |
| /** |
| * Encrypts or decrypts data in a single-part operation, or finishes a |
| * multiple-part operation. The data is encrypted or decrypted, depending on how |
| * this cipher was initialized. |
| * |
| * @param inBuffer the input ByteBuffer |
| * @param outBuffer the output ByteBuffer |
| * @return int number of bytes stored in {@code output} |
| * @throws BadPaddingException if this cipher is in decryption mode, and |
| * (un)padding has been requested, but the |
| * decrypted data is not bounded by the |
| * appropriate padding bytes |
| * @throws IllegalBlockSizeException if this cipher is a block cipher, no |
| * padding has been requested (only in |
| * encryption mode), and the total input |
| * length of the data processed by this cipher |
| * is not a multiple of block size; or if this |
| * encryption algorithm is unable to process |
| * the input data provided. |
| * @throws ShortBufferException if the given output buffer is too small to |
| * hold the result |
| */ |
| @Override |
| public int doFinal(final ByteBuffer inBuffer, final ByteBuffer outBuffer) |
| throws ShortBufferException, IllegalBlockSizeException, BadPaddingException { |
| final int uptLen = update(inBuffer, outBuffer); |
| final int[] outlen = new int[1]; |
| final int retVal = OpenSslNativeJna.EVP_CipherFinal_ex(context, outBuffer, outlen); |
| throwOnError(retVal); |
| final int len = uptLen + outlen[0]; |
| outBuffer.position(outBuffer.position() + outlen[0]); |
| return len; |
| } |
| |
| /** |
| * Encrypts or decrypts data in a single-part operation, or finishes a |
| * multiple-part operation. |
| * |
| * @param input the input byte array |
| * @param inputOffset the offset in input where the input starts |
| * @param inputLen the input length |
| * @param output the byte array for the result |
| * @param outputOffset the offset in output where the result is stored |
| * @return the number of bytes stored in output |
| * @throws ShortBufferException if the given output byte array is too small |
| * to hold the result |
| * @throws BadPaddingException if this cipher is in decryption mode, and |
| * (un)padding has been requested, but the |
| * decrypted data is not bounded by the |
| * appropriate padding bytes |
| * @throws IllegalBlockSizeException if this cipher is a block cipher, no |
| * padding has been requested (only in |
| * encryption mode), and the total input |
| * length of the data processed by this cipher |
| * is not a multiple of block size; or if this |
| * encryption algorithm is unable to process |
| * the input data provided. |
| */ |
| @Override |
| public int doFinal(final byte[] input, final int inputOffset, final int inputLen, final byte[] output, |
| final int outputOffset) throws ShortBufferException, IllegalBlockSizeException, BadPaddingException { |
| final ByteBuffer outputBuf = ByteBuffer.wrap(output, outputOffset, output.length - outputOffset); |
| final ByteBuffer inputBuf = ByteBuffer.wrap(input, inputOffset, inputLen); |
| return doFinal(inputBuf, outputBuf); |
| } |
| |
| /** |
| * Continues a multi-part update of the Additional Authentication Data (AAD). |
| * <p> |
| * Calls to this method provide AAD to the opensslEngine when operating in modes |
| * such as AEAD (GCM). If this opensslEngine is operating in either GCM mode, |
| * all AAD must be supplied before beginning operations on the ciphertext (via |
| * the {@code update} and {@code doFinal} methods). |
| * |
| * @param aad the buffer containing the Additional Authentication Data |
| * |
| * @throws IllegalArgumentException if the {@code aad} byte array is null |
| * @throws IllegalStateException if this opensslEngine is in a wrong |
| * state (e.g., has not been initialized), |
| * does not accept AAD, or if operating in |
| * either GCM mode and one of the |
| * {@code update} methods has already been |
| * called for the active |
| * encryption/decryption operation |
| * @throws UnsupportedOperationException if the implementation |
| * {@code opensslEngine} doesn't support |
| * this operation. |
| */ |
| @Override |
| public void updateAAD(final byte[] aad) |
| throws IllegalArgumentException, IllegalStateException, UnsupportedOperationException { |
| // TODO: implement GCM mode using Jna |
| throw new UnsupportedOperationException("This is unsupported in Jna Cipher"); |
| } |
| |
| /** |
| * Continues a multi-part update of the Additional Authentication Data (AAD). |
| * <p> |
| * Calls to this method provide AAD to the opensslEngine when operating in modes |
| * such as AEAD (GCM). If this opensslEngine is operating in either GCM mode, |
| * all AAD must be supplied before beginning operations on the ciphertext (via |
| * the {@code update} and {@code doFinal} methods). |
| * |
| * @param aad the buffer containing the Additional Authentication Data |
| * |
| * @throws IllegalArgumentException if the {@code aad} byte array is null |
| * @throws IllegalStateException if this opensslEngine is in a wrong |
| * state (e.g., has not been initialized), |
| * does not accept AAD, or if operating in |
| * either GCM mode and one of the |
| * {@code update} methods has already been |
| * called for the active |
| * encryption/decryption operation |
| * @throws UnsupportedOperationException if the implementation |
| * {@code opensslEngine} doesn't support |
| * this operation. |
| */ |
| @Override |
| public void updateAAD(final ByteBuffer aad) |
| throws IllegalArgumentException, IllegalStateException, UnsupportedOperationException { |
| // TODO: implement GCM mode using Jna |
| throw new UnsupportedOperationException("This is unsupported in Jna Cipher"); |
| } |
| |
| /** |
| * Closes the OpenSSL cipher. Clean the OpenSsl native context. |
| */ |
| @Override |
| public void close() { |
| if (context != null) { |
| OpenSslNativeJna.EVP_CIPHER_CTX_cleanup(context); |
| // Freeing the context multiple times causes a JVM crash |
| // A work-round is to only free it at finalize time |
| // TODO is that sufficient? |
| // OpenSslNativeJna.EVP_CIPHER_CTX_free(context); |
| } |
| } |
| |
| /** |
| * @param retVal the result value of error. |
| */ |
| private void throwOnError(final int retVal) { |
| if (retVal != 1) { |
| final NativeLong err = OpenSslNativeJna.ERR_peek_error(); |
| final String errdesc = OpenSslNativeJna.ERR_error_string(err, null); |
| |
| if (context != null) { |
| OpenSslNativeJna.EVP_CIPHER_CTX_cleanup(context); |
| } |
| throw new IllegalStateException( |
| "return code " + retVal + " from OpenSSL. Err code is " + err + ": " + errdesc); |
| } |
| } |
| |
| // TODO DUPLICATED CODE, needs cleanup |
| /** Nested class for algorithm, mode and padding. */ |
| private static class Transform { |
| final String algorithm; |
| final String mode; |
| final String padding; |
| |
| /** |
| * Constructor of Transform. |
| * |
| * @param algorithm the algorithm name |
| * @param mode the mode name |
| * @param padding the padding name |
| */ |
| public Transform(final String algorithm, final String mode, final String padding) { |
| this.algorithm = algorithm; |
| this.mode = mode; |
| this.padding = padding; |
| } |
| } |
| |
| /** |
| * Tokenize the transformation. |
| * |
| * @param transformation current transformation |
| * @return the Transform |
| * @throws NoSuchAlgorithmException if the algorithm is not supported |
| */ |
| private static Transform tokenizeTransformation(final String transformation) throws NoSuchAlgorithmException { |
| if (transformation == null) { |
| throw new NoSuchAlgorithmException("No transformation given."); |
| } |
| |
| /* |
| * Array containing the components of a Cipher transformation: index 0: |
| * algorithm (e.g., AES) index 1: mode (e.g., CTR) index 2: padding (e.g., |
| * NoPadding) |
| */ |
| final String[] parts = new String[3]; |
| int count = 0; |
| final StringTokenizer parser = new StringTokenizer(transformation, "/"); |
| while (parser.hasMoreTokens() && count < 3) { |
| parts[count++] = parser.nextToken().trim(); |
| } |
| if (count != 3 || parser.hasMoreTokens()) { |
| throw new NoSuchAlgorithmException("Invalid transformation format: " + transformation); |
| } |
| return new Transform(parts[0], parts[1], parts[2]); |
| } |
| |
| /** |
| * AlgorithmMode of JNA. Currently only support AES/CTR/NoPadding. |
| */ |
| private enum AlgorithmMode { |
| AES_CTR, AES_CBC; |
| |
| /** |
| * Gets the AlgorithmMode instance. |
| * |
| * @param algorithm the algorithm name |
| * @param mode the mode name |
| * @return the AlgorithmMode instance |
| * @throws NoSuchAlgorithmException if the algorithm is not support |
| */ |
| static AlgorithmMode get(final String algorithm, final String mode) throws NoSuchAlgorithmException { |
| try { |
| return AlgorithmMode.valueOf(algorithm + "_" + mode); |
| } catch (final Exception e) { |
| throw new NoSuchAlgorithmException("Doesn't support algorithm: " + algorithm + " and mode: " + mode); |
| } |
| } |
| } |
| |
| /** |
| * Padding of JNA. |
| */ |
| private enum Padding { |
| NoPadding, PKCS5Padding; |
| |
| /** |
| * Gets the Padding instance. |
| * |
| * @param padding the padding name |
| * @return the AlgorithmMode instance |
| * @throws NoSuchPaddingException if the algorithm is not support |
| */ |
| static int get(final String padding) throws NoSuchPaddingException { |
| try { |
| return Padding.valueOf(padding).ordinal(); |
| } catch (final Exception e) { |
| throw new NoSuchPaddingException("Doesn't support padding: " + padding); |
| } |
| } |
| } |
| |
| @Override |
| public int getBlockSize() { |
| return CryptoCipherFactory.AES_BLOCK_SIZE; |
| } |
| |
| @Override |
| public String getAlgorithm() { |
| return transformation; |
| } |
| |
| @Override |
| protected void finalize() throws Throwable { |
| OpenSslNativeJna.EVP_CIPHER_CTX_free(context); |
| super.finalize(); |
| } |
| } |