/* ************************************************************************ # # DivConq # # http://divconq.com/ # # Copyright: # Copyright 2014 eTimeline, LLC. All rights reserved. # # License: # See the license.txt file in the project's top-level directory for details. # # Authors: # * Andy White # ************************************************************************ */ package divconq.pgp; import io.netty.buffer.ByteBuf; import java.io.BufferedInputStream; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; import org.bouncycastle.bcpg.ContainedPacket; import org.bouncycastle.bcpg.PacketTags; import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; import org.bouncycastle.jcajce.util.DefaultJcaJceHelper; import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator; import divconq.hub.Hub; import divconq.lang.chars.Utf8Encoder; import divconq.pgp.PGPUtil; public class EncryptedFileStream { public static final int MAX_PACKET_SIZE = 32 * 1024; public static final int MAX_PARTIAL_LEN = 0xEF; protected String fileName = "temp.bin"; protected long modificationTime = System.currentTimeMillis(); //protected PGPPublicKey pubKey = null; protected int algorithm = PGPEncryptedData.AES_256; protected List<PGPKeyEncryptionMethodGenerator> methods = new ArrayList<>(); protected boolean writeFirst = false; protected boolean isClosed = false; protected SecureRandom rand = new SecureRandom(); protected byte[] key = null; protected Cipher cipher = null; protected MessageDigest digest = null; protected int packetsize = 0; protected int packetpos = 0; protected ByteBuf packetbuf = null; protected ByteBuf out = null; protected List<ByteBuf> readyBuffers = new ArrayList<>(); public void setFileName(String fileName) { this.fileName = fileName; } public String getFileName() { return this.fileName; } public boolean isClosed() { return this.isClosed; } public void setModificationTime(long modificationTime) { this.modificationTime = modificationTime; } public long getModificationTime() { return this.modificationTime; } public void setAlgorithm(int algorithm) { this.algorithm = algorithm; } public int getAlgorithm() { return this.algorithm; } public void addPublicKey(PGPPublicKey pubKey) { this.methods.add(new JcePublicKeyKeyEncryptionMethodGenerator(pubKey)); } public void addMethod(PGPKeyEncryptionMethodGenerator v) { this.methods.add(v); } public void ensureBuffer(int size) { if ((this.out == null) || (this.out.writableBytes() < size)) this.allocNextBuffer(size); } public void allocNextBuffer() { this.allocNextBuffer(1); // use default } public void allocNextBuffer(int size) { if (this.out != null) this.readyBuffers.add(this.out); // buffer must be no larger than 1 GB and should probably be at least 4 KB so we can fit initial sections (packets) // at top of file // create buffers that are at least 32KB in size this.out = Hub.instance.getBufferAllocator().heapBuffer(Math.max(size, 32 * 1024)); // TODO config } public ByteBuf nextReadyBuffer() { if (this.readyBuffers.size() > 0) return this.readyBuffers.remove(0); return null; } public void loadPublicKey(Path keyring) throws IOException, PGPException { // TODO move some of this to dcPGPUtil PGPPublicKey pubKey = null; InputStream keyIn = new BufferedInputStream(new FileInputStream(keyring.toFile())); PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(org.bouncycastle.openpgp.PGPUtil.getDecoderStream(keyIn), new JcaKeyFingerprintCalculator()); // // we just loop through the collection till we find a key suitable for encryption, in the real // world you would probably want to be a bit smarter about this. // @SuppressWarnings("rawtypes") Iterator keyRingIter = pgpPub.getKeyRings(); while (keyRingIter.hasNext() && (pubKey == null)) { PGPPublicKeyRing keyRing = (PGPPublicKeyRing)keyRingIter.next(); @SuppressWarnings("rawtypes") Iterator keyIter = keyRing.getPublicKeys(); while (keyIter.hasNext() && (pubKey == null)) { PGPPublicKey key = (PGPPublicKey)keyIter.next(); if (key.isEncryptionKey()) pubKey = key; } } if (pubKey == null) throw new IllegalArgumentException("Can't find encryption key in key ring."); this.methods.add(new JcePublicKeyKeyEncryptionMethodGenerator(pubKey)); } // means writing to section that is ciphered only and never compressed, use writeCompressed // most of the time even if not using compression public void writeData(byte[] bytes, int offset, int len) { // the first time this is called we need to write headers - those headers // call into this method so clear flag immediately if (!this.writeFirst) { this.writeFirst = true; this.writeFirstLiteral(len); } int remaining = len; int avail = this.packetsize - this.packetpos; // packetbuf may have data that has not yet been processed, so if we are doing any writes // we need to write the packet buffer first ByteBuf pbb = this.packetbuf; if (pbb != null) { int bbremaining = pbb.readableBytes(); // only write if there is space available in current packet or if we have a total // amount of data larger than max packet size while ((bbremaining > 0) && ((avail > 0) || (bbremaining + remaining) >= MAX_PACKET_SIZE)) { // out of current packet space? create more packets if (avail == 0) { this.packetsize = MAX_PACKET_SIZE; this.packetpos = 0; this.writeDataInternal((byte) MAX_PARTIAL_LEN); // partial packet length avail = this.packetsize; } // figure out how much we can write to the current packet, write it, update indexes int alen = Math.min(avail, bbremaining); this.writeDataInternal(pbb.array(), pbb.arrayOffset() + pbb.readerIndex(), alen); pbb.skipBytes(alen); bbremaining = pbb.readableBytes(); this.packetpos += alen; avail = this.packetsize - this.packetpos; // our formula always assumes that packetbuf starts at zero offset, anytime // we write out part of the packetbuf we either need to write it all and clear it // or we need to start with a new buffer with data starting at offset 0 if (bbremaining == 0) { pbb.clear(); } else { ByteBuf npb = Hub.instance.getBufferAllocator().heapBuffer(MAX_PACKET_SIZE); npb.writeBytes(pbb, bbremaining); this.packetbuf = npb; pbb.release(); pbb = npb; } } } // only write if there is space available in current packet or if we have a total // amount of data larger than max packet size while ((remaining > 0) && ((avail > 0) || (remaining >= MAX_PACKET_SIZE))) { // out of current packet space? create more packets if (avail == 0) { this.packetsize = MAX_PACKET_SIZE; this.packetpos = 0; this.writeDataInternal((byte) MAX_PARTIAL_LEN); // partial packet length avail = this.packetsize; } // figure out how much we can write to the current packet, write it, update indexes int alen = Math.min(avail, remaining); this.writeDataInternal(bytes, offset, alen); remaining -= alen; offset += alen; this.packetpos += alen; avail = this.packetsize - this.packetpos; } // buffer remaining to build larger packet later if (remaining > 0) { if (this.packetbuf == null) this.packetbuf = Hub.instance.getBufferAllocator().heapBuffer(MAX_PACKET_SIZE); // add to new buffer or add to existing buffer, either way it should be less than max here this.packetbuf.writeBytes(bytes, offset, remaining); } } public void writeData(byte val) { byte[] bytes = new byte[1]; bytes[0] = val; this.writeData(bytes, 0, 1); } public void writeData(ByteBuf buf) { this.writeData(buf.array(), buf.arrayOffset() + buf.readerIndex(), buf.readableBytes()); } // writes without checking if we need to add a new packet length, useful for headers and tags protected void writeDataInternal(byte[] bytes, int offset, int len) { if (this.algorithm != SymmetricKeyAlgorithmTags.NULL) { // hash the unprocessed bytes this.digest.update(bytes, offset, len); // TODO add debugging support by capturing bytes before compression // TODO add compression support // if compression flag is on, then send data through compressor // encrypt the data byte[] cout = this.cipher.update(bytes, offset, len); if (cout == null) return; int cpos = 0; // fill present buffer as much as possible if (this.out.writableBytes() > 0) { int clen = Math.min(cout.length, this.out.writableBytes()); this.out.writeBytes(cout, cpos, clen); cpos += clen; } int remaining = cout.length - cpos; if (remaining == 0) return; this.ensureBuffer(remaining); this.out.writeBytes(cout, cpos, remaining); } else { // TODO add debugging support by capturing bytes before compression // TODO add compression support // if compression flag is on, then send data through compressor // fill present buffer as much as possible if (this.out.writableBytes() > 0) { int clen = Math.min(len, this.out.writableBytes()); this.out.writeBytes(bytes, offset, clen); offset += clen; len -= clen; } if (len == 0) return; this.ensureBuffer(len); this.out.writeBytes(bytes, offset, len); } } protected void writeDataInternal(byte val) { byte[] bytes = new byte[1]; bytes[0] = val; this.writeDataInternal(bytes, 0, 1); } // call before putting any file data in buffer public void init() throws PGPException, IOException, InvalidKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchPaddingException { if (this.algorithm != SymmetricKeyAlgorithmTags.NULL) { // ******************************************************************* // public key packet(s) // ******************************************************************* if ((methods.size() == 1) && (methods.get(0) instanceof PBEKeyEncryptionMethodGenerator)) { PBEKeyEncryptionMethodGenerator method = (PBEKeyEncryptionMethodGenerator)methods.get(0); this.key = method.getKey(algorithm); ContainedPacket packet1 = method.generate(algorithm, null); byte[] encoded1 = packet1.getEncoded(); this.ensureBuffer(encoded1.length); this.out.writeBytes(encoded1); } else { this.key = org.bouncycastle.openpgp.PGPUtil.makeRandomKey(algorithm, rand); byte[] sessionInfo = new byte[key.length + 3]; // add algorithm sessionInfo[0] = (byte) algorithm; // add key System.arraycopy(key, 0, sessionInfo, 1, key.length); // add checksum int check = 0; for (int i = 1; i != sessionInfo.length - 2; i++) check += sessionInfo[i] & 0xff; sessionInfo[sessionInfo.length - 2] = (byte)(check >> 8); sessionInfo[sessionInfo.length - 1] = (byte)(check); for (PGPKeyEncryptionMethodGenerator method : methods) { ContainedPacket packet1 = method.generate(algorithm, sessionInfo); byte[] encoded1 = packet1.getEncoded(); this.ensureBuffer(encoded1.length); this.out.writeBytes(encoded1); } } // ******************************************************************* // encrypt packet, add IV to encryption though // ******************************************************************* this.ensureBuffer(3); this.out.writeByte(0xC0 | PacketTags.SYM_ENC_INTEGRITY_PRO); this.out.writeByte(0); // unknown size this.out.writeByte(1); // version number // ******************** start encryption ********************** String cName = PGPUtil.getSymmetricCipherName(algorithm) + "/CFB/NoPadding"; DefaultJcaJceHelper helper = new DefaultJcaJceHelper(); this.cipher = helper.createCipher(cName); byte[] iv = new byte[this.cipher.getBlockSize()]; this.cipher.init(Cipher.ENCRYPT_MODE, PGPUtil.makeSymmetricKey(algorithm, key), new IvParameterSpec(iv)); this.digest = MessageDigest.getInstance("SHA-1"); // --- encrypt checksum for encrypt packet, part of the encrypted output --- byte[] inLineIv = new byte[this.cipher.getBlockSize() + 2]; rand.nextBytes(inLineIv); inLineIv[inLineIv.length - 1] = inLineIv[inLineIv.length - 3]; inLineIv[inLineIv.length - 2] = inLineIv[inLineIv.length - 4]; this.writeDataInternal(inLineIv, 0, inLineIv.length); } // ******************* Optionally add Compression ************************** // TODO set compressor // ******************** Literal data packet *********************** this.ensureBuffer(1); this.writeDataInternal((byte) (0xC0 | PacketTags.LITERAL_DATA)); } public void writeFirstLiteral(int dataLength) { // --- data packet --- byte[] encName = Utf8Encoder.encode(this.fileName); int headerlen = 1 // format + 1 // name length + encName.length // file name + 4; // time // if less than 512 assume this is all there will be, because we cannot stream with numbers smaller than 512 for initial if (dataLength < (512 - headerlen)) { this.packetsize = dataLength + headerlen; this.writeDataPacketLength(this.packetsize); } else { int length = dataLength + headerlen; int power = 0; for (power = 0; (length != 1) && (power < 16); power++) length >>>= 1; this.packetsize = 1 << power; this.writeDataInternal((byte) (0xE0 | power)); // partial packet length } byte[] hdr = new byte[headerlen]; hdr[0] = (byte)PGPLiteralData.BINARY; // data format hdr[1] = (byte)encName.length; // file name for (int i = 0; i < encName.length; i++) hdr[2 + i] = encName[i]; hdr[headerlen - 4] = (byte)(this.modificationTime >> 24); hdr[headerlen - 3] = (byte)(this.modificationTime >> 16); hdr[headerlen - 2] = (byte)(this.modificationTime >> 8); hdr[headerlen - 1] = (byte)this.modificationTime; this.writeDataInternal(hdr, 0, headerlen); this.packetpos = headerlen; } /* * Finish writing out the current packet and add protection packet */ public void close() throws PGPException { if (this.isClosed) return; this.isClosed = true; if (this.packetbuf != null) { // flush data if any packet space is available this.writeData(new byte[0], 0, 0); this.packetsize = this.packetbuf.readableBytes(); this.packetpos = 0; // even if zero, this is fine, we need a final packet this.writeDataPacketLength(this.packetsize); if (this.packetsize > 0) this.writeData(new byte[0], 0, 0); this.packetbuf.release(); this.packetbuf = null; } if (this.algorithm != SymmetricKeyAlgorithmTags.NULL) { this.ensureBuffer(22); this.writeDataInternal((byte) (0xC0 | PacketTags.MOD_DETECTION_CODE)); this.writeDataInternal((byte) 20); // length of SHA-1 is always 20 bytes this.writeDataInternal(this.digest.digest(), 0, 20); // TODO final compression, pass into doFinal below byte[] fcipher; try { fcipher = this.cipher.doFinal(); } catch (Exception x) { throw new PGPException("Problem with PGP cipher", x); } this.ensureBuffer(fcipher.length); this.out.writeBytes(fcipher); // write raw } else { // TODO final compression, if any } this.readyBuffers.add(this.out); this.out = null; } public void writeDataPacketLength(int bodyLen) { if (bodyLen < 192) { this.writeDataInternal((byte) bodyLen); } else if (bodyLen <= 8383) { bodyLen -= 192; int oct1 = ((bodyLen >> 8) & 0xff) + 192; this.writeDataInternal((byte) oct1); this.writeDataInternal((byte) bodyLen); } else { this.writeDataInternal((byte) 0xff); this.writeDataInternal((byte) (bodyLen >> 24)); this.writeDataInternal((byte) (bodyLen >> 16)); this.writeDataInternal((byte) (bodyLen >> 8)); this.writeDataInternal((byte) bodyLen); } } /* * Reverse length: * if (newPacket) { tag = hdr & 0x3f; int l = this.read(); if (l < 192) { bodyLen = l; } else if (l <= 223) { int b = in.read(); bodyLen = ((l - 192) << 8) + (b) + 192; } else if (l == 255) { bodyLen = (in.read() << 24) | (in.read() << 16) | (in.read() << 8) | in.read(); } else { partial = true; bodyLen = 1 << (l & 0x1f); } } else { int lengthType = hdr & 0x3; tag = (hdr & 0x3f) >> 2; switch (lengthType) { case 0: bodyLen = this.read(); break; case 1: bodyLen = (this.read() << 8) | this.read(); break; case 2: bodyLen = (this.read() << 24) | (this.read() << 16) | (this.read() << 8) | this.read(); break; case 3: partial = true; break; default: throw new IOException("unknown length type encountered"); } } * */ }