Java tutorial
/* 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)); } } } }