/* ************************************************************************ # # 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.test.pgp; import io.netty.buffer.ByteBuf; import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import org.bouncycastle.bcpg.ContainedPacket; import org.bouncycastle.bcpg.PacketTags; 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.operator.PBEKeyEncryptionMethodGenerator; import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator; import divconq.hub.Hub; import divconq.lang.chars.Utf8Encoder; import divconq.pgp.PGPUtil; import divconq.util.HexUtil; /** * For use with simple scenario where you want to fill buffers containing * a single encrypted file. Give us a key and we'll AES-256 encrypt * your content with MDC integrity protection (SHA-1). * * Hand us buffers of data or a stream - make sure the stream is small * though since everything you give us will go into memory. * * Primary methods are: open, close, writeBuffer and writeStream. * * @author Andy White */ /* * TODO example and Utility call for above - File in to File out * * With no compression * :pubkey enc packet: version 3, algo 1, keyid D983468282E667B0 data: [2047 bits] user: "Andy White <awhite@filetransferconsulting.com>" 2048-bit RSA key, ID 82E667B0, created 2014-10-20 :encrypted data packet: length: unknown mdc_method: 2 gpg: encrypted with 2048-bit RSA key, ID 82E667B0, created 2014-10-20 "Andy White <awhite@filetransferconsulting.com>" :literal data packet: mode b (62), created 1414169807, name="story.xml", raw data: unknown length * * * * with compression * :pubkey enc packet: version 3, algo 1, keyid D983468282E667B0 data: [2047 bits] user: "Andy White <awhite@filetransferconsulting.com>" 2048-bit RSA key, ID 82E667B0, created 2014-10-20 :encrypted data packet: length: 47110 mdc_method: 2 gpg: encrypted with 2048-bit RSA key, ID 82E667B0, created 2014-10-20 "Andy White <awhite@filetransferconsulting.com>" :compressed packet: algo=1 :literal data packet: mode b (62), created 1413810619, name="story.xml", raw data: unknown length * */ public class PGPWriter { static public final int BUF_SIZE_POWER = 16; // 2^16 size buffer on long files protected String fileName = "temp.bin"; protected long modificationTime = System.currentTimeMillis(); protected SecureRandom rand = new SecureRandom(); protected int algorithm = 0; protected byte[] key = null; protected Cipher cipher = null; protected ByteBuf out = null; protected List<ByteBuf> readyBuffers = new ArrayList<>(); /* * Create a stream representing a general packet. * * @param out buffer to write to * @param tag type of packet * @param length expected content length * @throws IOException */ public void startGeneralPacket(int tag, long length) throws IOException { int hdr = 0x80; hdr |= 0x40 | tag; out.writeByte(hdr); this.writeNewPacketLength(length); } /* * Create a new style partial input stream buffered into chunks. * * @param out buffer to write to * @param tag packet tag. * @param buffer size of chunks making up the packet. * @throws IOException */ public void startPartialPacket(int tag) throws IOException { int hdr = 0xC0 | tag; // packet tag out.writeByte(hdr); System.out.println("Started " + tag + " which is " + hdr + " hex: " + HexUtil.charToHex(hdr)); // unknown length //this.out.writeZero(1); /* this.partialBuffer = new byte[64 * 1024]; // TODO configure or something, use bytebuf int length = this.partialBuffer.length; for (this.partialPower = 0; length != 1; this.partialPower++) length >>>= 1; if (this.partialPower > 30) throw new IOException("Buffer cannot be greater than 2^30 in length."); this.partialBufferLength = 1 << this.partialPower; this.partialOffset = 0; */ } public void writeNewPacketLength(long bodyLen) throws IOException { if (bodyLen < 192) { out.writeByte((int) bodyLen); } else if (bodyLen <= 8383) { bodyLen -= 192; out.writeByte((int)(((bodyLen >> 8) & 0xff) + 192)); out.writeByte((int)bodyLen); } else { out.writeByte(0xff); out.writeByte((int)(bodyLen >> 24)); out.writeByte((int)(bodyLen >> 16)); out.writeByte((int)(bodyLen >> 8)); out.writeByte((int)bodyLen); } } /* NOT NEEDED public void writePacket(int tag, byte[] body) throws IOException { this.writeHeader(tag, false, body.length); out.writeBytes(body); } */ /* REVIEW public void finishPartial() throws IOException { if (this.partialBuffer != null) { this.partialFlush(true); this.partialBuffer = null; } } public void partialFlush(boolean isLast) throws IOException { if (isLast) { this.writeNewPacketLength(this.partialOffset); out.writeBytes(this.partialBuffer, 0, this.partialOffset); } else { out.writeByte(0xE0 | this.partialPower); out.writeBytes(this.partialBuffer, 0, this.partialBufferLength); } this.partialOffset = 0; } public void writePartial(byte b) throws IOException { if (this.partialOffset == this.partialBufferLength) this.partialFlush(false); this.partialBuffer[partialOffset++] = b; } public void writePartial(byte[] buf, int off, int len) throws IOException { if (this.partialOffset == this.partialBufferLength) this.partialFlush(false); if (len <= (this.partialBufferLength - this.partialOffset)) { System.arraycopy(buf, off, this.partialBuffer, this.partialOffset, len); this.partialOffset += len; } else { System.arraycopy(buf, off, this.partialBuffer, this.partialOffset, this.partialBufferLength - this.partialOffset); off += this.partialBufferLength - this.partialOffset; len -= this.partialBufferLength - this.partialOffset; this.partialFlush(false); while (len > this.partialBufferLength) { System.arraycopy(buf, off, this.partialBuffer, 0, this.partialBufferLength); off += this.partialBufferLength; len -= this.partialBufferLength; this.partialFlush(false); } System.arraycopy(buf, off, this.partialBuffer, 0, len); this.partialOffset += len; } } public void write(int b) throws IOException { if (this.partialBuffer != null) this.writePartial((byte)b); else out.writeByte(b); } public void write(byte[] bytes, int off, int len) throws IOException { if (this.partialBuffer != null) this.writePartial(bytes, off, len); else out.writeBytes(bytes, off, len); } */ public void writeStream(InputStream in) throws IOException { while (true) { /* TODO byte[] byte[] any = this.cipher.update(inLineIv); if (any != null) this.out.writeBytes(any); // we may include this in digest, TODO review */ int avail = this.out.writableBytes(); int actual = this.out.writeBytes(in, avail); if (this.out.writableBytes() == 0) this.allocNextBuffer(); if (actual < avail) break; } in.close(); } /* consider this in some future improvement protected void writeObject(BCPGObject o) throws IOException { out.writeBytes(o.getEncoded()); // TODO this is inefficient, fix someday. } */ /* * this is for non-literal data sections */ protected void writePacket(ContainedPacket p) throws IOException { byte[] encoded = p.getEncoded(); // TODO this is inefficient, fix someday. this.ensureBuffer(encoded.length); this.out.writeBytes(encoded); } public void ensureBuffer(int size) { if ((this.out == null) || (this.out.writableBytes() < size)) this.allocNextBuffer(); } public void allocNextBuffer() { 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 this.out = Hub.instance.getBufferAllocator().heapBuffer(256 * 1024); // TODO config / smaller!!! } /** * Start here with an open for a given recipient (public) key. * * @param key encrypt to this key * @throws IOException if there are problems encoding the packets * @throws PGPException problems with key or crypto */ public void open(PGPPublicKey key) throws IOException, PGPException { this.open(PGPEncryptedData.AES_256, new JcePublicKeyKeyEncryptionMethodGenerator(key)); } public void open(int algorithm, PGPKeyEncryptionMethodGenerator... methods) throws IOException, PGPException, IllegalStateException { if (methods.length == 0) throw new IllegalStateException("no encryption methods specified"); this.algorithm = algorithm; // ******************************************************************* // public key packet // ******************************************************************* // TODO this condition untested and perhaps not helpful, review (PBE - password based encryption) if ((methods.length == 1) && (methods[0] instanceof PBEKeyEncryptionMethodGenerator)) { PBEKeyEncryptionMethodGenerator m = (PBEKeyEncryptionMethodGenerator)methods[0]; this.key = m.getKey(algorithm); this.writePacket(m.generate(algorithm, null)); } else { this.key = org.bouncycastle.openpgp.PGPUtil.makeRandomKey(algorithm, rand); byte[] sessionInfo = this.createSessionInfo(); for (int i = 0; i < methods.length; i++) { PGPKeyEncryptionMethodGenerator m = (PGPKeyEncryptionMethodGenerator)methods[i]; this.writePacket(m.generate(algorithm, sessionInfo)); } } int packet1 = this.out.writerIndex(); System.out.println("packet 1: " + packet1 + " final bytes: " + HexUtil.bufferToHex(this.out.array(), this.out.arrayOffset() + packet1 - 5, 5)); // ******************************************************************* // encrypt packet, add IV to // ******************************************************************* try { 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, this.key), new IvParameterSpec(iv)); // // we have to add block size + 2 for the generated IV and + 1 + 22 if integrity protected // this.ensureBuffer(this.cipher.getBlockSize() + 2 + 1 + 22); this.startPartialPacket(PacketTags.SYM_ENC_INTEGRITY_PRO); //, this.cipher.getBlockSize() + 2 + 1 + 22); this.out.writeByte(1); // version number byte[] inLineIv = new byte[this.cipher.getBlockSize() + 2]; this.rand.nextBytes(inLineIv); inLineIv[inLineIv.length - 1] = inLineIv[inLineIv.length - 3]; inLineIv[inLineIv.length - 2] = inLineIv[inLineIv.length - 4]; // TODO byte[] any = this.cipher.update(inLineIv); if (any != null) this.out.writeBytes(any); // we may include this in digest, TODO review } catch (InvalidKeyException e) { throw new PGPException("invalid key: " + e.getMessage(), e); } catch (InvalidAlgorithmParameterException e) { throw new PGPException("imvalid algorithm parameter: " + e.getMessage(), e); } catch (GeneralSecurityException e) { throw new PGPException("cannot create cipher: " + e.getMessage(), e); } int packet2 = this.out.writerIndex(); System.out.println("packet 2: first bytes: " + HexUtil.bufferToHex(this.out.array(), this.out.arrayOffset() + packet1, 25)); System.out.println("packet 2: " + packet2 + " final bytes: " + HexUtil.bufferToHex(this.out.array(), this.out.arrayOffset() + packet2 - 5, 5)); // ******************************************************************* // TODO compress packet, if any // ******************************************************************* int packet3 = this.out.writerIndex(); //System.out.println("packet 3: first bytes: " + HexUtil.bufferToHex(this.out.array(), this.out.arrayOffset() + packet2, 25)); //System.out.println("packet 3: " + packet3 + " final bytes: " + HexUtil.bufferToHex(this.out.array(), this.out.arrayOffset() + packet3 - 5, 5)); // ******************************************************************* // literal packet start // ******************************************************************* this.startPartialPacket(PacketTags.LITERAL_DATA); byte[] encName = Utf8Encoder.encode(this.fileName); // TODO don't hard code int len = 1 + 1 + encName.length + 4 + 99; // type + name length + name + mod time + file content //out.writeByte(len); this.writeNewPacketLength(len); out.writeByte(PGPLiteralData.BINARY); out.writeByte((byte)encName.length); out.writeBytes(encName); long modDate = this.modificationTime / 1000; out.writeByte((byte)(modDate >> 24)); out.writeByte((byte)(modDate >> 16)); out.writeByte((byte)(modDate >> 8)); out.writeByte((byte)(modDate)); int packet4 = this.out.writerIndex(); System.out.println("packet 4: first bytes: " + HexUtil.bufferToHex(this.out.array(), this.out.arrayOffset() + packet3, 25)); System.out.println("packet 4: " + packet4 + " final bytes: " + HexUtil.bufferToHex(this.out.array(), this.out.arrayOffset() + packet4 - 5, 5)); // TODO new SHA1PGPDigestCalculator(); //if (digestCalc != null) // genOut = new TeeOutputStream(digestCalc.getOutputStream(), genOut); } protected byte[] createSessionInfo() { byte[] sessionInfo = new byte[this.key.length + 3]; // add algorithm sessionInfo[0] = (byte) this.algorithm; // add key System.arraycopy(this.key, 0, sessionInfo, 1, this.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); return sessionInfo; } /* * Finish writing out the current packet without closing the underlying stream. */ public void close() throws IOException { // TODO review this.finishPartial(); this.ensureBuffer(22); this.startGeneralPacket(PacketTags.MOD_DETECTION_CODE, 20); /* byte[] dig = this.dc.getDigest(); */ this.out.writeBytes(new byte[20]); // TODO this.readyBuffers.add(this.out); this.out = null; } public ByteBuf nextReadyBuffer() { if (this.readyBuffers.size() > 0) return this.readyBuffers.remove(0); return null; } }