blob: 76b4c15f9c6dc40643db171f44b6a05574d14d66 [file] [log] [blame]
/*
* 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.solr.encryption.crypto;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.security.AccessController;
import java.security.PrivilegedAction;
import org.apache.lucene.util.SuppressForbidden;
import static org.apache.solr.encryption.crypto.AesCtrUtil.*;
/**
* Hack {@link AesCtrEncrypter} equivalent to {@link javax.crypto.Cipher} with "AES/CTR/NoPadding" but more efficient.
* <p>The {@link #LightAesCtrEncrypter(byte[], byte[]) constructor} and the {@link #init(long)} operations are lighter and
* faster; {@link #clone()} is much faster. But it needs to call internal private {@code com.sun.crypto.provider.CounterMode}
* with reflection. It may not be {@link #isSupported() supported}.
* <p>Why do we need to access private {@code com.sun.crypto.provider.CounterMode} and {@code com.sun.crypto.provider.AESCrypt}?
* Because they contain the special JVM annotation @HotSpotIntrinsicCandidate that makes their encryption method extremely
* fast. If we copy the code in pure Java, it runs 30x slower.
*/
public class LightAesCtrEncrypter implements AesCtrEncrypter {
/**
* {@link LightAesCtrEncrypter} factory.
*/
public static final AesCtrEncrypterFactory FACTORY = new Factory();
private static final Constructor<?> AES_CRYPT_CONSTRUCTOR;
private static final Method AES_CRYPT_INIT_METHOD;
private static final Constructor<?> COUNTER_MODE_CONSTRUCTOR;
private static final Field COUNTER_MODE_IV_FIELD;
private static final Method COUNTER_MODE_RESET_METHOD;
private static final Method COUNTER_MODE_CRYPT_METHOD;
private static final Throwable HACK_FAILURE;
static {
Hack hack = AccessController.doPrivileged((PrivilegedAction<Hack>) LightAesCtrEncrypter::hack);
AES_CRYPT_CONSTRUCTOR = hack.aesCryptConstructor;
AES_CRYPT_INIT_METHOD = hack.aesCryptInitMethod;
COUNTER_MODE_CONSTRUCTOR = hack.counterModeConstructor;
COUNTER_MODE_IV_FIELD = hack.counterIvField;
COUNTER_MODE_RESET_METHOD = hack.counterModeResetMethod;
COUNTER_MODE_CRYPT_METHOD = hack.counterModeCryptMethod;
HACK_FAILURE = hack.hackFailure;
}
private final Object aesCrypt;
private final byte[] initialIv;
private Object counterMode;
private byte[] iv;
/**
* Indicates whether the {@link LightAesCtrEncrypter} hack is supported.
* If it is not supported, then {@link LightAesCtrEncrypter} constructor throws an {@link UnsupportedOperationException}
* (with the hack failure cause).
*/
public static boolean isSupported() {
return HACK_FAILURE == null;
}
/**
* @param key The encryption key. It is cloned internally, its content is not modified, and no reference to it is kept.
* @param iv The Initialization Vector (IV) for the CTR mode. It MUST be random for the effectiveness of the encryption.
* It can be public (for example stored clear at the beginning of the encrypted file). It is cloned internally,
* its content is not modified, and no reference to it is kept.
* @throws UnsupportedOperationException If the hack is not {@link #isSupported() supported}.
*/
public LightAesCtrEncrypter(byte[] key, byte[] iv) {
if (HACK_FAILURE != null) {
throw new UnsupportedOperationException(HACK_FAILURE);
}
checkAesKey(key);
try {
aesCrypt = AES_CRYPT_CONSTRUCTOR.newInstance();
AES_CRYPT_INIT_METHOD.invoke(aesCrypt, false, "AES", key);
counterMode = COUNTER_MODE_CONSTRUCTOR.newInstance(aesCrypt);
} catch (Exception e) {
throw new RuntimeException(e);
}
this.initialIv = iv.clone();
this.iv = iv.clone();
}
@Override
public void init(long counter) {
checkCtrCounter(counter);
buildAesCtrIv(initialIv, counter, iv);
try {
COUNTER_MODE_IV_FIELD.set(counterMode, iv);
COUNTER_MODE_RESET_METHOD.invoke(counterMode);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void process(ByteBuffer inBuffer, ByteBuffer outBuffer) {
assert inBuffer.array() != outBuffer.array() : "Input and output buffers must not be backed by the same array";
int length = inBuffer.remaining();
if (length > outBuffer.remaining()) {
throw new IllegalArgumentException("Output buffer does not have enough remaining space (needs " + length + " B)");
}
int outPos = outBuffer.position();
try {
COUNTER_MODE_CRYPT_METHOD.invoke(counterMode, inBuffer.array(), inBuffer.arrayOffset() + inBuffer.position(),
length, outBuffer.array(), outBuffer.arrayOffset() + outPos);
} catch (Exception e) {
throw new RuntimeException(e);
}
inBuffer.position(inBuffer.limit());
outBuffer.position(outPos + length);
}
@Override
public LightAesCtrEncrypter clone() {
LightAesCtrEncrypter clone;
try {
clone = (LightAesCtrEncrypter) super.clone();
} catch (CloneNotSupportedException e) {
throw new Error("Failed to clone " + LightAesCtrEncrypter.class.getSimpleName() + "; this should not happen");
}
// aesCrypt and initialIv are the same references.
try {
clone.counterMode = COUNTER_MODE_CONSTRUCTOR.newInstance(aesCrypt);
} catch (Exception e) {
throw new RuntimeException(e);
}
clone.iv = initialIv.clone();
return clone;
}
@SuppressForbidden(reason = "Needs access to private APIs in com.sun.crypto.provider.CounterMode and com.sun.crypto.provider.AESCrypt to enable the hack")
private static Hack hack() {
Hack hack = new Hack();
try {
Class<?> aesCryptClass = Class.forName("com.sun.crypto.provider.AESCrypt");
hack.aesCryptConstructor = aesCryptClass.getDeclaredConstructor();
hack.aesCryptConstructor.setAccessible(true);
hack.aesCryptInitMethod = aesCryptClass.getDeclaredMethod("init", boolean.class, String.class, byte[].class);
hack.aesCryptInitMethod.setAccessible(true);
Class<?> counterModeClass = Class.forName("com.sun.crypto.provider.CounterMode");
Class<?> symmetricCipherClass = Class.forName("com.sun.crypto.provider.SymmetricCipher");
hack.counterModeConstructor = counterModeClass.getDeclaredConstructor(symmetricCipherClass);
hack.counterModeConstructor.setAccessible(true);
Class<?> feedbackCipherClass = Class.forName("com.sun.crypto.provider.FeedbackCipher");
hack.counterIvField = feedbackCipherClass.getDeclaredField("iv");
hack.counterIvField.setAccessible(true);
hack.counterModeResetMethod = counterModeClass.getDeclaredMethod("reset");
hack.counterModeResetMethod.setAccessible(true);
hack.counterModeCryptMethod = counterModeClass.getDeclaredMethod("implCrypt", byte[].class, int.class, int.class, byte[].class, int.class);
hack.counterModeCryptMethod.setAccessible(true);
} catch (SecurityException se) {
hack.hackFailure = new UnsupportedOperationException(LightAesCtrEncrypter.class.getName() + " is not supported"
+ " because not all required permissions are given to the Encryption JAR file: " + se +
" [To support it, grant at least the following permissions:" +
" RuntimePermission(\"accessClassInPackage.com.sun.crypto.provider\") " +
" and ReflectPermission(\"suppressAccessChecks\")]", se);
} catch (ReflectiveOperationException | RuntimeException e) {
hack.hackFailure = new UnsupportedOperationException(LightAesCtrEncrypter.class.getName() + " is not supported"
+ " on this platform because internal Java APIs are not compatible with this Solr version: " + e, e);
}
if (hack.hackFailure != null) {
hack.aesCryptConstructor = null;
hack.aesCryptInitMethod = null;
hack.counterModeConstructor = null;
hack.counterIvField = null;
hack.counterModeResetMethod = null;
hack.counterModeCryptMethod = null;
}
return hack;
}
private static class Hack {
Constructor<?> aesCryptConstructor;
Method aesCryptInitMethod;
Constructor<?> counterModeConstructor;
Field counterIvField;
Method counterModeResetMethod;
Method counterModeCryptMethod;
UnsupportedOperationException hackFailure;
}
/**
* {@link LightAesCtrEncrypter} factory.
*/
public static class Factory implements AesCtrEncrypterFactory {
@Override
public AesCtrEncrypter create(byte[] key, byte[] iv) {
return new LightAesCtrEncrypter(key, iv);
}
@Override
public boolean isSupported() {
return LightAesCtrEncrypter.isSupported();
}
@Override
public Throwable getUnsupportedCause() {
return LightAesCtrEncrypter.HACK_FAILURE;
}
}
}