CrystalKeep/src/main/java/nl/andrewlalis/crystalkeep/io/ClusterIO.java

150 lines
5.4 KiB
Java

package nl.andrewlalis.crystalkeep.io;
import nl.andrewlalis.crystalkeep.io.serialization.ByteUtils;
import nl.andrewlalis.crystalkeep.io.serialization.ClusterSerializer;
import nl.andrewlalis.crystalkeep.model.Cluster;
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
/**
* This class is responsible for reading and writing clusters to files, which
* may be encrypted with a password-based AES-256 CBC.
*
* <p>
* All saved files start with a single byte with value 1 to indicate that
* the file is encrypted, or 0 if it is not encrypted. If encrypted, the
* next 8 bytes will contain the salt that was used to encrypt the file.
* Encrypted files will then contain a 16 byte initialization vector.
* The remaining part of the file is simply the contents.
* </p>
*
* @see nl.andrewlalis.crystalkeep.io.serialization.ClusterSerializer
*/
public class ClusterIO {
public static final Path CLUSTER_PATH = Path.of(System.getProperty("user.home"), ".crystalkeep", "clusters");
public static final int FILE_VERSION = 1;
static {
try {
Files.createDirectories(CLUSTER_PATH);
} catch (IOException e) {
e.printStackTrace();
}
}
private final SecureRandom random;
public ClusterIO() {
try {
this.random = SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Could not initialize secure random.", e);
}
}
/**
* Loads an encrypted cluster from the given path, using the given password
* as the secret key value.
* @param path The path of the file to load from.
* @param password The password to use to decrypt the contents.
* @return The cluster that was loaded.
* @throws Exception If the file could not be found, or could not be
* decrypted and read properly.
*/
public Cluster load(Path path, char[] password) throws Exception {
try (var is = Files.newInputStream(path)) {
int version = ByteUtils.readInt(is);
if (version < FILE_VERSION) {
System.err.println("Warning! Reading older file version: " + version);
}
int encryptionFlag = is.read();
if (encryptionFlag == 0) throw new IOException("File is not encrypted.");
byte[] salt = is.readNBytes(8);
byte[] iv = is.readNBytes(16);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, this.getSecretKey(password, salt), new IvParameterSpec(iv));
try (CipherInputStream cis = new CipherInputStream(is, cipher)) {
return ClusterSerializer.readCluster(cis);
}
}
}
/**
* Saves a cluster to an encrypted file, using the given password as the
* secret key.
* @param cluster The cluster to save.
* @param path The path to the file to save the cluster at.
* @param password The password to use when saving.
* @throws IOException If an error occurs when writing the file.
* @throws GeneralSecurityException If we could not obtain a secret key.
*/
public void save(Cluster cluster, Path path, char[] password) throws IOException, GeneralSecurityException {
Files.createDirectories(path.getParent());
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
byte[] iv = new byte[16];
this.random.nextBytes(iv);
byte[] salt = new byte[8];
this.random.nextBytes(salt);
cipher.init(Cipher.ENCRYPT_MODE, this.getSecretKey(password, salt), new IvParameterSpec(iv));
try (OutputStream fos = Files.newOutputStream(path)) {
ByteUtils.writeInt(fos, FILE_VERSION);
fos.write(1);
fos.write(salt);
fos.write(iv);
try (CipherOutputStream cos = new CipherOutputStream(fos, cipher)) {
ClusterSerializer.writeCluster(cluster, cos);
}
}
}
/**
* Saves an unencrypted cluster file.
* @param cluster The cluster to save.
* @param path The file to save the cluster to.
* @throws IOException If the file could not be saved.
*/
public void saveUnencrypted(Cluster cluster, Path path) throws IOException {
Files.createDirectories(path.getParent());
try (var os = Files.newOutputStream(path)) {
ByteUtils.writeInt(os, FILE_VERSION);
os.write(0);
ClusterSerializer.writeCluster(cluster, os);
}
}
/**
* Loads an unencrypted cluster from a file.
* @param path The path to load the cluster from.
* @return The cluster that was loaded.
* @throws IOException If the file could not be loaded.
*/
public Cluster loadUnencrypted(Path path) throws IOException {
try (var is = Files.newInputStream(path)) {
int version = ByteUtils.readInt(is);
if (version < FILE_VERSION) {
System.err.println("Warning! Reading older file version: " + version);
}
int encryptionFlag = is.read();
if (encryptionFlag == 1) throw new IOException("File is encrypted.");
return ClusterSerializer.readCluster(is);
}
}
private SecretKey getSecretKey(char[] password, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password, salt, 65536, 256);
return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
}
}