/*
* This file is part of the Illarion project.
*
* Copyright © 2015 - Illarion e.V.
*
* Illarion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Illarion 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.
*/
package illarion.common.util;
import org.jetbrains.annotations.Contract;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.WillNotClose;
import javax.crypto.*;
import java.io.*;
import java.security.Key;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
/**
* Class to handle the encryption of the files that are stored by the client.
* The encryption created by this class bases on a private and a public key.
*
* @author Nop
* @author Martin Karing <nitram@illarion.org>
*/
@SuppressWarnings("SpellCheckingInspection")
public final class Crypto {
/**
* The error and debug logger of the client.
*/
@Nonnull
private static final Logger log = LoggerFactory.getLogger(Crypto.class);
/**
* The filename of the private key.
*/
@Nonnull
private static final String PRIVATE_KEY = "private.key";
/**
* The filename of the public key.
*/
@Nonnull
private static final String PUBLIC_KEY = "public.key";
/**
* String for the transformation name "RSA"
*/
@Nonnull
private static final String KEY_ALGORITHM = "RSA";
/**
* The size of the buffer used to transfer the data during encryption and decryption.
*/
private static final int TRANSFER_BUFFER_SIZE = 4096;
/**
* The private key instance that was loaded into the this class.
*/
@Nullable
private PrivateKey privateKey;
/**
* The public key instance that was loaded into the this class.
*/
@Nullable
private PublicKey publicKey;
/**
* Get a stream that delivers the decrypted data.
*
* @param src the stream that delivers the encryted data
* @return the stream that provides the decrypted data
* @throws CryptoException
*/
@Nonnull
public InputStream getDecryptedStream(@Nonnull @WillNotClose InputStream src) throws CryptoException {
if (!hasPublicKey()) {
throw new IllegalStateException("No keys loaded");
}
try {
//noinspection IOResourceOpenedButNotSafelyClosed,resource
DataInputStream dIn = new DataInputStream(src);
int keyLength = dIn.readInt();
byte[] wrappedKey = new byte[keyLength];
int n = 0;
while (n < keyLength) {
n += dIn.read(wrappedKey, n, keyLength - n);
}
@Nonnull Cipher wrappingCipher = Cipher.getInstance(KEY_ALGORITHM);
wrappingCipher.init(Cipher.UNWRAP_MODE, publicKey);
Key encryptionKey = wrappingCipher.unwrap(wrappedKey, "DES", Cipher.SECRET_KEY);
@Nonnull Cipher cipher = Cipher.getInstance("DES");
cipher.init(Cipher.DECRYPT_MODE, encryptionKey);
return new CipherInputStream(src, cipher);
} catch (@Nonnull Exception e) {
throw new CryptoException(e);
}
}
/**
* Transfer all bytes from a input stream to a output stream.
*
* @param in the source input stream
* @param out the target output stream
* @throws IOException in case reading from the source stream or writing to the target stream fails
*/
private static void transferBytes(@Nonnull InputStream in, @Nonnull OutputStream out) throws IOException {
byte[] buffer = new byte[TRANSFER_BUFFER_SIZE];
int n = in.read(buffer);
while (n > -1) {
out.write(buffer, 0, n);
n = in.read(buffer);
}
out.flush();
}
/**
* Encrypt a data stream with the the private key. This is only possible in
* case the configuration tool constructed this class with both keys, the
* public key and the private key.
*
* @param src data to encrypt
* @param dst target stream for encryption
*/
public void encrypt(@Nonnull InputStream src, @Nonnull OutputStream dst) throws CryptoException {
if (!hasPrivateKey()) {
throw new IllegalStateException("No keys loaded");
}
try (OutputStream cOutStream = getEncryptedStream(new NonClosingOutputStream(dst))) {
transferBytes(src, cOutStream);
} catch (@Nonnull Exception e) {
throw new CryptoException(e);
}
}
/**
* Get a stream that takes unencrypted data and forwards it encrypted.
*
* @param dst the stream that is supposed to recieve the encrypted data
* @return the system that will receive the unencrypted data
* @throws CryptoException
*/
@Nonnull
public OutputStream getEncryptedStream(@Nonnull OutputStream dst) throws CryptoException {
if (!hasPrivateKey()) {
throw new IllegalStateException("No keys loaded");
}
try {
// generate a random DES key
KeyGenerator keygen = KeyGenerator.getInstance("DES");
SecureRandom random = new SecureRandom();
keygen.init(random);
SecretKey key = keygen.generateKey();
// wrap with RSA public key
@Nonnull Cipher wrappingCipher = Cipher.getInstance(KEY_ALGORITHM);
wrappingCipher.init(Cipher.WRAP_MODE, privateKey);
byte[] wrappedKey = wrappingCipher.wrap(key);
try (DataOutputStream out = new DataOutputStream(dst)) {
out.writeInt(wrappedKey.length);
out.write(wrappedKey);
// encrypt data
@Nonnull Cipher cipher = Cipher.getInstance("DES");
cipher.init(Cipher.ENCRYPT_MODE, key);
return new CipherOutputStream(out, cipher);
}
} catch (@Nonnull Exception e) {
throw new CryptoException(e);
}
}
/**
* Check if this class has a private key that can be used to encrypt data.
*
* @return {@code true} in case there is a private key loaded
*/
public boolean hasPrivateKey() {
return privateKey != null;
}
/**
* Check if this class has a public key that can be used to decrypt data.
*
* @return {@code true} in case there is a public key loaded
*/
public boolean hasPublicKey() {
return publicKey != null;
}
/**
* Get a file from the resources embedded in the classpath.
*
* @param name the name of the file
* @return the stream to read the file or {@code null} in case the file was not found
*/
@Nullable
private static InputStream getResourceAsStream(@Nonnull String name) {
return Thread.currentThread().getContextClassLoader().getResourceAsStream(name);
}
/**
* Load the private key from any source available.
*/
public void loadPrivateKey() {
privateKey = loadKeyImpl(PRIVATE_KEY);
if (hasPrivateKey()) {
return;
}
log.error("Loading the private key failed.");
}
/**
* Load the private key from a input stream.
*
* @param in the input stream the load the private key from
*/
public void loadPrivateKey(InputStream in) {
privateKey = loadKeyImpl(in);
if (hasPrivateKey()) {
return;
}
log.error("Loading the private key failed.");
}
/**
* Load the public key from any source available.
*/
public void loadPublicKey() {
publicKey = loadKeyImpl(PUBLIC_KEY);
if (hasPublicKey()) {
return;
}
log.error("Loading the public key failed.");
}
/**
* Load a key from a string reference. This function will be check both the file system and the class path for
* this file.
*
* @param keyFile the reference to the key file
* @param <T> the type of the key that is expected
* @return the loaded key or {@code null} in case the key was not found
*/
@Nullable
private static <T extends Key> T loadKeyImpl(@Nonnull String keyFile) {
T resourceKey = loadKeyImpl(getResourceAsStream(keyFile));
if (resourceKey != null) {
return resourceKey;
}
File fileRef = new File(keyFile);
if (fileRef.exists() && fileRef.isFile() && fileRef.canRead()) {
InputStream keyIn = null;
try {
keyIn = new BufferedInputStream(new FileInputStream(keyFile));
T fileKey = loadKeyImpl(keyIn);
if (fileKey != null) {
return fileKey;
}
} catch (@Nonnull Exception ignored) {
// loading the key failed
} finally {
if (keyIn != null) {
try {
keyIn.close();
} catch (@Nonnull IOException ignored) {
}
}
}
}
return null;
}
/**
* Load the key from a input stream.
*
* @param in the input stream the load the private key from
* @return {@code true} in case the key was loaded successfully
*/
@Nullable
@Contract("null->null")
private static <T extends Key> T loadKeyImpl(@Nullable InputStream in) {
if (in != null) {
try (ObjectInput keyIn = new ObjectInputStream(in)) {
Object keyObject = keyIn.readObject();
//noinspection unchecked
return (T) keyObject;
} catch (@Nonnull Exception ignored) {
// loading the key failed
}
}
return null;
}
}