All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dsa.iot.dslink.serializer.SerializationManager Maven / Gradle / Ivy

The newest version!
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);
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy