package org.dsa.iot.dslink.serializer;
import org.dsa.iot.dslink.node.*;
import org.dsa.iot.dslink.provider.*;
import org.dsa.iot.dslink.util.*;
import org.dsa.iot.dslink.util.json.*;
import org.slf4j.*;
import java.io.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import javax.crypto.*;
import javax.crypto.spec.*;
/**
* Handles automatic serialization and deserialization.
*
* @author Samuel Grenier
*/
public class SerializationManager {
private static final Logger LOGGER;
private final File file;
private final File backup;
private final Deserializer deserializer;
private final Serializer serializer;
private ScheduledFuture<?> future;
private SecretKeySpec secretKeySpec;
private static final String PASSWORD_PREFIX = "\u001Bpw:";
static final String PASSWORD_TOKEN = "assword";
private final AtomicBoolean changed = new AtomicBoolean(false);
/**
* Handles serialization based on the file path.
*
* @param file Path that holds the data
* @param manager Manager to deserialize/serialize
*/
public SerializationManager(File file, NodeManager manager) {
this.file = file;
this.backup = new File(file.getPath() + ".bak");
this.deserializer = new Deserializer(this, manager);
this.serializer = new Serializer(this, manager);
}
public void markChanged() {
changed.set(true);
}
public void markChangedOverride(boolean bool) {
changed.set(bool);
}
public synchronized void start() {
stop();
future = LoopProvider.getProvider().schedulePeriodic(new Runnable() {
@Override
public void run() {
boolean c = changed.getAndSet(false);
if (c) {
serialize();
}
}
}, 5, 5, TimeUnit.SECONDS);
}
public synchronized void stop() {
if (future != null) {
future.cancel(false);
future = null;
}
}
/**
* Serializes the data from the node manager into the file based on the
* path. Manually calling this is redundant as a timer will automatically
* handle serialization.
*/
public void serialize() {
try {
JsonObject json = serializer.serialize();
//Save the config db to a temp file. If we can't do that, then we don't
//want to do anything else.
File tmp = new File(file.getParent(), file.getName() + ".tmp");
if (tmp.exists()) {
if (!tmp.delete()) {
throw new IOException("Could not delete " + tmp.getName());
}
}
FileUtils.write(tmp, json.encodePrettily());
if (!tmp.exists()) {
throw new IOException(
tmp.getName() + " weirdly did not exist after writing to it");
}
if (tmp.length() == 0) {
throw new IOException(tmp.getName() + " serialized to a file size of 0");
}
//Now backup the last version
if (file.exists()) {
if (backup.exists()) {
if (!backup.delete()) {
throw new IOException(
"Could not delete old " + backup.getName());
}
}
LOGGER.debug("Making backup");
if (!file.renameTo(backup)) {
FileUtils.copy(file, backup); //try really hard
}
if (!backup.exists()) {
throw new IllegalStateException(
"Unable to make backup " + backup.getName());
}
if (file.exists()) {
if (!file.delete()) {
throw new IOException("Could not delete old " + file.getName());
}
}
}
//Move the new tmp database into position.
if (!tmp.renameTo(file)) {
FileUtils.copy(tmp, file); //try really hard
}
if (!file.exists()) {
//This will keep the tmp file in place, although the next serialization
//attempt will probably delete it
throw new IOException(
"Failed to move " + tmp.getName() + " to " + file.getName());
}
if (tmp.exists()) {
if (!tmp.delete()) {
LOGGER.warn("Unable to delete old tmp file " + tmp.getName());
}
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Backup complete");
}
} catch (IOException e) {
LOGGER.error("Failed to save configuration database", e);
}
}
/**
* Deserializes the data into the node manager based on the path.
*
* @throws Exception An error has occurred deserializing the nodes.
*/
public void deserialize() throws Exception {
if (file.exists()) {
try {
handle(FileUtils.readAllBytes(file));
LOGGER.debug("Restored " + file.getName());
return;
} catch (Exception x) {
LOGGER.error("Could not deserialize " + file.getName(), x);
}
}
//There was a problem with the primary db.
if (backup.exists()) {
try {
handle(FileUtils.readAllBytes(backup));
LOGGER.warn("Restored backup " + backup.getName());
if (file.exists()) {
//Try delete the primary db so it won't overwrite the
//good backup during the next serialization
if (!file.delete()) {
LOGGER.warn("Unable to delete corrupt " + file.getName());
}
}
return;
} catch (Exception x) {
LOGGER.error("Could not delete " + file.getName(), x);
}
}
//We've got nothing to lose, try the tmp serialization file
File tmp = new File(file.getParent(), file.getName() + ".tmp");
if (tmp.exists()) {
try {
handle(FileUtils.readAllBytes(tmp));
LOGGER.warn("Restored " + tmp.getName());
return;
} catch (Exception x) {
LOGGER.error("Could not deserialize " + tmp.getName(), x);
}
}
LOGGER.warn("Unable to deserialize a configuration database");
}
private void handle(byte[] bytes) throws Exception {
String in = new String(bytes, "UTF-8");
JsonObject obj = new JsonObject(in);
deserializer.deserialize(obj);
}
/**
* Decrypts passwords that were encrypted by the encrypt method. This is backwards
* compatible with older unencrypted passwords.
*
* @param pass Base64 encoding of the password to decrypt, can be encrypted or
* unencrypted.
* @return An unencrypted password.
*/
synchronized String decrypt(Node node, String pass) {
try {
if (pass.startsWith(PASSWORD_PREFIX)) {
byte[] bytes = UrlBase64.decode(pass.substring(PASSWORD_PREFIX.length()));
bytes = applyCipher(bytes, node, Cipher.DECRYPT_MODE);
pass = new String(bytes, "UTF-8");
}
} catch (Exception x) {
throw new RuntimeException(x);
}
return pass;
}
/**
* Encrypts passwords using characters from the private key of the link as
* the secret key.
*
* @param pass Unencrypted password.
* @return Base64 encoding of the encrypted password.
*/
synchronized String encrypt(Node node, String pass) {
try {
byte[] bytes = pass.getBytes("UTF-8");
bytes = applyCipher(bytes, node, Cipher.ENCRYPT_MODE);
return PASSWORD_PREFIX + UrlBase64.encode(bytes);
} catch (Exception x) {
throw new RuntimeException(x);
}
}
/**
* Encrypts or decrypts the given password.
*
* @param password The password to encrypt or decrypt.
* @param node Used to get the private key of the link.
* @param cipherMode Cipher.ENCRYPT_MODE or Cipher.DECRYPT_MODE
* @return The transformed password.
*/
private byte[] applyCipher(byte[] password, Node node, int cipherMode)
throws Exception {
final String ALGO = "AES";
if (secretKeySpec == null) {
byte[] privateKey = node.getLink().getHandler()
.getConfig().getKeys().getPrivateKey().getEncoded();
final int KEY_LEN = 16;
byte[] key = new byte[KEY_LEN];
System.arraycopy(privateKey, privateKey.length - KEY_LEN, key, 0, KEY_LEN);
secretKeySpec = new SecretKeySpec(key, ALGO);
}
Cipher cipher = Cipher.getInstance(ALGO);
cipher.init(cipherMode, secretKeySpec);
return cipher.doFinal(password);
}
static {
LOGGER = LoggerFactory.getLogger(SerializationManager.class);
}
}