de.tntinteractive.portalsammler.engine.SecureStore.java Source code

Java tutorial

Introduction

Here is the source code for de.tntinteractive.portalsammler.engine.SecureStore.java

Source

/*
Copyright (C) 2013  Tobias Baum <tbaum at tntinteractive.de>
    
This file is a part of Portalsammler.
    
Portalsammler is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
    
Portalsammler 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.
    
You should have received a copy of the GNU General Public License
along with Portalsammler.  If not, see <http://www.gnu.org/licenses/>.
 */
package de.tntinteractive.portalsammler.engine;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
import java.util.zip.InflaterOutputStream;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.tuple.Pair;

public final class SecureStore {

    private StorageLayer storage;
    private final SecureRandom srand;
    private final byte[] key;
    private final int sizeLimit = 1024 * 1024;

    private final Settings settings = new Settings();
    private int settingSalt;

    private final DocumentIndex index = new DocumentIndex();
    private int indexSalt;

    private ByteArrayOutputStream currentOutputBuffer;
    private int currentFileIndex;

    private SecureStore(final StorageLayer storage, final SecureRandom srand, final byte[] key) {
        this.storage = storage;
        this.srand = srand;
        this.key = key;
    }

    public static SecureStore createEmpty(final StorageLayer storage, final SecureRandom srand, final byte[] key) {
        final SecureStore ret = new SecureStore(storage, srand, key);
        ret.settingSalt = ret.srand.nextInt();
        ret.currentOutputBuffer = new ByteArrayOutputStream();
        ret.currentFileIndex = 1;
        return ret;
    }

    public static SecureStore readFrom(final StorageLayer storage, final SecureRandom srand, final byte[] key)
            throws IOException, GeneralSecurityException {

        final SecureStore ret = new SecureStore(storage, srand, key);

        readSettings(storage, srand, key, ret);
        readIndex(storage, srand, key, ret);
        ret.setCurrentFileIndexToHighestKnown();
        ret.currentOutputBuffer = new ByteArrayOutputStream();
        if (storage.fileExists(ret.getCurrentFilename())) {
            ret.currentOutputBuffer.write(ret.readAndDecrypt(ret.getCurrentFilename()));
        } else {
            writeInt(ret.currentOutputBuffer, srand.nextInt());
        }
        ret.checkCurrentBufferSize();
        return ret;
    }

    private void setCurrentFileIndexToHighestKnown() {
        this.currentFileIndex = 1;
        do {
            this.currentFileIndex++;
        } while (this.storage.fileExists(this.getCurrentFilename()));
        this.currentFileIndex--;
    }

    private static void readSettings(final StorageLayer storage, final SecureRandom srand, final byte[] key,
            final SecureStore ret) throws IOException {
        final InputStream stream = storage.openInputStream("meta");
        try {
            final Pair<Integer, MapReader> saltAndReader = createMapReader(stream, srand, key);
            ret.settingSalt = saltAndReader.getLeft();
            final MapReader r = saltAndReader.getRight();

            Pair<String, Map<String, String>> p;
            while ((p = r.readNext()) != null) {
                final SourceSettings s = new SourceSettings(p.getRight());
                ret.getSettings().putSettings(p.getLeft(), s);
            }

            r.close();
        } finally {
            stream.close();
        }
    }

    private static void readIndex(final StorageLayer storage, final SecureRandom srand, final byte[] key,
            final SecureStore ret) throws IOException {
        final InputStream stream = storage.openInputStream("index");
        try {
            final Pair<Integer, MapReader> saltAndReader = createMapReader(stream, srand, key);
            ret.indexSalt = saltAndReader.getLeft();
            final MapReader r = saltAndReader.getRight();

            Pair<String, Map<String, String>> p;
            while ((p = r.readNext()) != null) {
                final DocumentInfo di = DocumentInfo.parse(p.getLeft());
                ret.index.putDocument(di, p.getRight());
            }

            r.close();
        } finally {
            stream.close();
        }
    }

    private static Pair<Integer, MapReader> createMapReader(final InputStream stream, final SecureRandom srand,
            final byte[] key) throws IOException {

        final InputStream cipher = CryptoHelper.createAesDecryptStream(stream, key, srand);
        final int salt = readInt(cipher);

        final InflaterInputStream inflate = new InflaterInputStream(cipher);
        return Pair.of(salt, MapReader.createFrom(inflate));
    }

    public Settings getSettings() {
        return this.settings;
    }

    public boolean containsDocument(final DocumentInfo info) {
        return this.index.getAllDocuments().contains(info);
    }

    public void storeDocument(final DocumentInfo metadata, final byte[] content) throws IOException {
        final Map<String, String> fileOffset = this.saveContent(content);
        fileOffset.put("u", "u");
        this.index.putDocument(metadata, fileOffset);
    }

    private Map<String, String> saveContent(final byte[] content) throws IOException {
        this.checkCurrentBufferSize();

        final int offset = this.currentOutputBuffer.size();
        final byte[] compressedContent = this.compress(content);
        this.currentOutputBuffer.write(compressedContent);

        final Map<String, String> fileOffset = new HashMap<String, String>();
        fileOffset.put("f", this.getCurrentFilename());
        fileOffset.put("o", Integer.toString(offset));
        fileOffset.put("s", Integer.toString(compressedContent.length));
        this.saveCurrentBuffer();

        return fileOffset;
    }

    private void checkCurrentBufferSize() throws IOException {
        if (this.currentOutputBuffer.size() > this.sizeLimit) {
            this.startNewOutputBuffer();
        }
    }

    private void saveCurrentBuffer() throws IOException {
        this.encryptAndWrite(this.key, this.currentOutputBuffer.toByteArray(), this.getCurrentFilename());
    }

    private void encryptAndWrite(final byte[] encryptionKey, final byte[] data, final String filename)
            throws IOException {

        final OutputStream out = this.storage.openOutputStream(filename);
        try {
            final OutputStream cipher = CryptoHelper.createAesEncryptStream(out, encryptionKey, this.srand);
            cipher.write(data);
            cipher.close();
        } finally {
            out.close();
        }
    }

    private void startNewOutputBuffer() throws IOException {
        this.currentOutputBuffer = new ByteArrayOutputStream();
        writeInt(this.currentOutputBuffer, this.srand.nextInt());
        this.currentFileIndex++;
    }

    private String getCurrentFilename() {
        return String.format("data.%08d", this.currentFileIndex);
    }

    private byte[] compress(final byte[] content) throws IOException {
        final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        final DeflaterOutputStream deflate = new DeflaterOutputStream(buffer);
        deflate.write(content);
        deflate.close();
        return buffer.toByteArray();
    }

    public byte[] getDocument(final DocumentInfo metadata) throws IOException {
        final Map<String, String> pointer = this.index.getFilePosition(metadata);
        final byte[] buffer = this.readAndDecrypt(pointer.get("f"));
        final int offset = Integer.parseInt(pointer.get("o"));
        final int size = Integer.parseInt(pointer.get("s"));

        final ByteArrayOutputStream result = new ByteArrayOutputStream();
        final InflaterOutputStream inflate = new InflaterOutputStream(result);
        inflate.write(buffer, offset, size);
        inflate.close();

        return result.toByteArray();
    }

    private byte[] readAndDecrypt(final String filename) throws IOException {
        final InputStream input = this.storage.openInputStream(filename);
        try {
            final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            final InputStream cipher = CryptoHelper.createAesDecryptStream(input, this.key, this.srand);
            IOUtils.copy(cipher, buffer);
            return buffer.toByteArray();
        } finally {
            input.close();
        }
    }

    /**
     * Schreibt alle Metadaten und ersetzt dadurch die zuletzt geschriebenen.
     */
    public void writeMetadata() throws IOException, GeneralSecurityException {
        this.writeSettings();
        this.writeIndex();
    }

    private void writeSettings() throws IOException {
        final OutputStream out = this.storage.openOutputStream("meta");
        try {
            final MapWriter w = this.createMapWriterAndWriteSalt(out, this.settingSalt);
            for (final String id : this.getSettings().getAllSettingIds()) {
                w.write(id, this.getSettings().getSettings(id).toStringMap());
            }
            w.close();
        } finally {
            out.close();
        }
    }

    private void writeIndex() throws IOException {
        final OutputStream out = this.storage.openOutputStream("index");
        try {
            final MapWriter w = this.createMapWriterAndWriteSalt(out, this.indexSalt);
            for (final DocumentInfo id : this.index.getAllDocuments()) {
                w.write(id.asString(), this.index.getFilePosition(id));
            }
            w.close();
        } finally {
            out.close();
        }
    }

    private MapWriter createMapWriterAndWriteSalt(final OutputStream out, final int salt) throws IOException {
        final OutputStream cipher = CryptoHelper.createAesEncryptStream(out, this.key, this.srand);
        writeInt(cipher, salt);

        final DeflaterOutputStream deflate = new DeflaterOutputStream(cipher);
        final MapWriter w = MapWriter.createFor(deflate);
        return w;
    }

    private static int readInt(final InputStream stream) throws IOException {
        final int ch1 = stream.read();
        final int ch2 = stream.read();
        final int ch3 = stream.read();
        final int ch4 = stream.read();
        return (ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0);
    }

    private static void writeInt(final OutputStream stream, final int v) throws IOException {
        stream.write((v >>> 24) & 0xFF);
        stream.write((v >>> 16) & 0xFF);
        stream.write((v >>> 8) & 0xFF);
        stream.write((v >>> 0) & 0xFF);
    }

    public DocumentIndex getIndex() {
        return this.index;
    }

    public boolean isRead(final DocumentInfo di) {
        final Map<String, String> fileInfo = this.index.getFilePosition(di);
        return fileInfo == null || fileInfo.get("u") == null;
    }

    public void markAsRead(final DocumentInfo di) {
        final Map<String, String> fileInfo = this.index.getFilePosition(di);
        if (fileInfo != null) {
            fileInfo.remove("u");
        }
    }

    public StorageLayer getDirectory() {
        return this.storage;
    }

    /**
     * Verschlsselt alle Daten neu mit dem bergebenen Schlssel und lscht die alten Dateien.
     * Dieser Store darf im Anschluss nicht mehr genutzt werden.
     * Es wird ein neuer Store mit dem neuen Schlssel erzeugt und zurckgeliefert.
     */
    public SecureStore recrypt(final byte[] newKey) throws IOException, GeneralSecurityException {
        this.createNewFiles(newKey);
        this.removeOldFiles();
        this.renameNewFiles();

        final SecureStore newStore = readFrom(this.storage, this.srand, newKey);
        this.storage = null;
        return newStore;
    }

    private void createNewFiles(final byte[] newKey) throws IOException {
        for (final String file : this.storage.getAllFiles()) {
            final byte[] data = this.readAndDecrypt(file);
            this.encryptAndWrite(newKey, data, file + ".tmp");
        }
    }

    private void removeOldFiles() throws IOException {
        for (final String file : this.storage.getAllFiles()) {
            if (!file.endsWith(".tmp")) {
                this.storage.delete(file);
            }
        }
    }

    private void renameNewFiles() throws IOException {
        for (final String file : this.storage.getAllFiles()) {
            if (file.endsWith(".tmp")) {
                this.storage.rename(file, file.substring(0, file.length() - 4));
            }
        }
    }

}