Added version number to file format.
This commit is contained in:
parent
3c6c165bc6
commit
50535d4bc0
|
@ -0,0 +1,10 @@
|
|||
# Cluster File Format Specification
|
||||
|
||||
This document describes the format of serialized cluster files. It is arranged as a sequential list of the data that is stored in a file.
|
||||
|
||||
* 4 bytes to indicate the version of the cluster file as an integer.
|
||||
* 1 byte with value 1 to indicate that the contents are encrypted, or 0 if unencrypted.
|
||||
* If encrypted:
|
||||
* 8 bytes contain the salt value used when encrypting.
|
||||
* 16 bytes contain the initialization vector used when encrypting.
|
||||
* The rest of the file contains the cluster content.
|
|
@ -6,6 +6,7 @@ import javafx.scene.Scene;
|
|||
import javafx.stage.Stage;
|
||||
import nl.andrewlalis.crystalkeep.control.MainViewController;
|
||||
import nl.andrewlalis.crystalkeep.model.Model;
|
||||
import nl.andrewlalis.crystalkeep.util.ImageCache;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
|
@ -22,6 +23,9 @@ public class CrystalKeep extends Application {
|
|||
var scene = new Scene(loader.load());
|
||||
stage.setScene(scene);
|
||||
stage.setTitle("CrystalKeep");
|
||||
ImageCache.get("/nl/andrewlalis/crystalkeep/ui/images/cluster_node_icon.png", 32, 32).ifPresent(img -> {
|
||||
stage.getIcons().add(img);
|
||||
});
|
||||
stage.sizeToScene();
|
||||
model.setActiveCluster(null); // TODO: Auto load last cluster?
|
||||
stage.show();
|
||||
|
|
|
@ -73,8 +73,11 @@ public class MainViewController implements ModelListener {
|
|||
try {
|
||||
if (password.isEmpty() || password.get().isEmpty()) {
|
||||
cluster = loader.loadUnencrypted(file.toPath());
|
||||
this.model.setActiveClusterPassword(null);
|
||||
} else {
|
||||
cluster = loader.load(file.toPath(), password.get());
|
||||
char[] pw = password.get().toCharArray();
|
||||
cluster = loader.load(file.toPath(), pw);
|
||||
this.model.setActiveClusterPassword(pw);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
|
@ -99,12 +102,15 @@ public class MainViewController implements ModelListener {
|
|||
if (file == null) return;
|
||||
path = file.toPath();
|
||||
}
|
||||
var password = this.promptPassword();
|
||||
char[] pw = this.model.getActiveClusterPassword();
|
||||
if (pw == null) {
|
||||
pw = this.promptPassword().orElse("").toCharArray();
|
||||
}
|
||||
try {
|
||||
if (password.isEmpty() || password.get().isEmpty()) {
|
||||
if (pw.length == 0) {
|
||||
loader.saveUnencrypted(model.getActiveCluster(), path);
|
||||
} else {
|
||||
loader.save(model.getActiveCluster(), path, password.get());
|
||||
loader.save(model.getActiveCluster(), path, pw);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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;
|
||||
|
||||
|
@ -33,6 +34,7 @@ import java.security.spec.KeySpec;
|
|||
*/
|
||||
public class ClusterIO {
|
||||
public static final Path CLUSTER_PATH = Path.of("clusters");
|
||||
public static final int FILE_VERSION = 1;
|
||||
|
||||
private final SecureRandom random;
|
||||
|
||||
|
@ -53,14 +55,19 @@ public class ClusterIO {
|
|||
* @throws Exception If the file could not be found, or could not be
|
||||
* decrypted and read properly.
|
||||
*/
|
||||
public Cluster load(Path path, String password) throws Exception {
|
||||
public Cluster load(Path path, char[] password) throws Exception {
|
||||
try (var is = Files.newInputStream(path)) {
|
||||
int version = ByteUtils.readInt(is);
|
||||
System.out.println("File version: " + version);
|
||||
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.toCharArray(), salt), new IvParameterSpec(iv));
|
||||
cipher.init(Cipher.DECRYPT_MODE, this.getSecretKey(password, salt), new IvParameterSpec(iv));
|
||||
try (CipherInputStream cis = new CipherInputStream(is, cipher)) {
|
||||
return ClusterSerializer.readCluster(cis);
|
||||
}
|
||||
|
@ -76,15 +83,16 @@ public class ClusterIO {
|
|||
* @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, String password) throws IOException, GeneralSecurityException {
|
||||
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.toCharArray(), salt), new IvParameterSpec(iv));
|
||||
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);
|
||||
|
@ -103,6 +111,7 @@ public class ClusterIO {
|
|||
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);
|
||||
}
|
||||
|
@ -116,6 +125,7 @@ public class ClusterIO {
|
|||
*/
|
||||
public Cluster loadUnencrypted(Path path) throws IOException {
|
||||
try (var is = Files.newInputStream(path)) {
|
||||
int version = ByteUtils.readInt(is);
|
||||
int encryptionFlag = is.read();
|
||||
if (encryptionFlag == 1) throw new IOException("File is encrypted.");
|
||||
return ClusterSerializer.readCluster(is);
|
||||
|
|
|
@ -17,6 +17,14 @@ public class ByteUtils {
|
|||
return ByteBuffer.wrap(bytes).getInt();
|
||||
}
|
||||
|
||||
public static int readInt(InputStream is) throws IOException {
|
||||
return toInt(is.readNBytes(4));
|
||||
}
|
||||
|
||||
public static void writeInt(OutputStream os, int value) throws IOException {
|
||||
os.write(toBytes(value));
|
||||
}
|
||||
|
||||
public static void writeLengthPrefixed(byte[] bytes, OutputStream os) throws IOException {
|
||||
os.write(toBytes(bytes.length));
|
||||
os.write(bytes);
|
||||
|
|
|
@ -8,6 +8,7 @@ import java.util.Set;
|
|||
public class Model {
|
||||
private Cluster activeCluster;
|
||||
private Path activeClusterPath;
|
||||
private char[] activeClusterPassword;
|
||||
|
||||
private final Set<ModelListener> listeners = new HashSet<>();
|
||||
|
||||
|
@ -32,6 +33,14 @@ public class Model {
|
|||
this.activeClusterPath = activeClusterPath;
|
||||
}
|
||||
|
||||
public char[] getActiveClusterPassword() {
|
||||
return activeClusterPassword;
|
||||
}
|
||||
|
||||
public void setActiveClusterPassword(char[] activeClusterPassword) {
|
||||
this.activeClusterPassword = activeClusterPassword;
|
||||
}
|
||||
|
||||
public void notifyListeners() {
|
||||
this.listeners.forEach(ModelListener::activeClusterUpdated);
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ public class ClusterIOTest {
|
|||
Cluster loaded = io.loadUnencrypted(file);
|
||||
assertEquals(cluster, loaded);
|
||||
// Test attempting to load an encrypted file, which throws an IOException.
|
||||
io.save(cluster, file, "testpass");
|
||||
io.save(cluster, file, "testpass".toCharArray());
|
||||
assertThrows(IOException.class, () -> io.loadUnencrypted(file));
|
||||
}
|
||||
|
||||
|
@ -59,14 +59,14 @@ public class ClusterIOTest {
|
|||
Path file = Files.createTempFile(UUID.randomUUID().toString(), "cts");
|
||||
var io = new ClusterIO();
|
||||
// Test normal save and load.
|
||||
io.save(cluster, file, "test");
|
||||
Cluster loaded = io.load(file, "test");
|
||||
io.save(cluster, file, "test".toCharArray());
|
||||
Cluster loaded = io.load(file, "test".toCharArray());
|
||||
assertEquals(cluster, loaded);
|
||||
// Test attempting to load an unencrypted file, which throws an IOException.
|
||||
io.saveUnencrypted(cluster, file);
|
||||
assertThrows(IOException.class, () -> io.load(file, "test"));
|
||||
assertThrows(IOException.class, () -> io.load(file, "test".toCharArray()));
|
||||
// Test attempting to load an encrypted file with the wrong password. An exception is thrown when reading it.
|
||||
io.save(cluster, file, "other");
|
||||
assertThrows(Exception.class, () -> io.load(file, "not_password"));
|
||||
io.save(cluster, file, "other".toCharArray());
|
||||
assertThrows(Exception.class, () -> io.load(file, "not_password".toCharArray()));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue