added lots of documentation
This commit is contained in:
34
README.md
Executable file
34
README.md
Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
# Java New I/O Crypto Library
|
||||||
|
|
||||||
|
The official distributions of [Java](https://openjdk.java.net/) include cryptographic functions in a framework called the [Java Cryptography Extension (JCE)](https://en.wikipedia.org/wiki/Java_Cryptography_Extension). JCE provides the API layer and corresponding implementations of cryptographic functions like key generation, key storage/retrieval, and cipher encryption/decryption. The JCE API supports those basic functions along with Java I/O based capabilities using `InputStream` and `OutputStream` implementations.
|
||||||
|
|
||||||
|
Since the introduction of JCE, Java has introduced the ["Non-blocking I/O" (NIO)](https://en.wikipedia.org/wiki/Non-blocking_I/O_%28Java%29) framework as a complement to the Java (blocking) I/O framework. It has huge advantages in performance for many applications. For instance, it is recommended that Apache Tomcat configurations use NIO connectors for clients due to its performance advantages.
|
||||||
|
|
||||||
|
## Using
|
||||||
|
|
||||||
|
### Including
|
||||||
|
|
||||||
|
To use this library, you must include it as a dependency to your project. An example configuration for Apache Maven is provided below. Either set the `inteligr8.nio-crypto.version` as a property or replace it with a valid version.
|
||||||
|
```xml
|
||||||
|
<project ..>
|
||||||
|
...
|
||||||
|
<dependencies>
|
||||||
|
...
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.inteligr8</groupId>
|
||||||
|
<artifactId>nio-crypto</artifactId>
|
||||||
|
<version>${inteligr8.nio-crypto.version}</version>
|
||||||
|
</dependency>
|
||||||
|
...
|
||||||
|
</dependencies>
|
||||||
|
...
|
||||||
|
</project>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Developing
|
||||||
|
|
||||||
|
There are many different algorithms that are available for encrypting and decrypting content. This project does not care about those details. It does not care about key generation, storage, or retrieval. It only cares about the streaming of content through a cipher. The default set of algorithms come from the JVM default JCE provider. You can include other JCE providers that provide the same, similar, and completely new algorithms. Some of those providers interface directly with hardware or the network for enhanced security or performance.
|
||||||
|
|
||||||
|
A [cipher](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/javax/crypto/Cipher.html) is defined by an algorithm, cipher mode, and padding. These are married together into a single parameter in JCE called a *transformation*. That same parameter is used in this library. You will also need an appropriate key and your content.
|
||||||
|
|
||||||
|
You can find [sample code for common algorithms in the source](/inteligr8/nio-crypto/src/stable/src/test/java/com/inteligr8/nio/CommonSamples.java).
|
@@ -14,8 +14,18 @@ import javax.crypto.Cipher;
|
|||||||
import javax.crypto.NoSuchPaddingException;
|
import javax.crypto.NoSuchPaddingException;
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base class for an encrypting or decrypting Java NIO ByteChannel.
|
||||||
|
*
|
||||||
|
* @author brian@inteligr8.com
|
||||||
|
*/
|
||||||
public class AbstractCryptoByteChannel {
|
public class AbstractCryptoByteChannel {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(AbstractCryptoByteChannel.class);
|
||||||
|
|
||||||
private static final String DEFAULT_CIPHER_MODE = "CBC";
|
private static final String DEFAULT_CIPHER_MODE = "CBC";
|
||||||
private static final String DEFAULT_CIPHER_PADDING = "PKCS5Padding";
|
private static final String DEFAULT_CIPHER_PADDING = "PKCS5Padding";
|
||||||
private static final Pattern ALGORITHM_PATTERN = Pattern.compile("(.+)/(.+)/(.+)");
|
private static final Pattern ALGORITHM_PATTERN = Pattern.compile("(.+)/(.+)/(.+)");
|
||||||
@@ -44,6 +54,8 @@ public class AbstractCryptoByteChannel {
|
|||||||
this.cipherMode = cipherMode;
|
this.cipherMode = cipherMode;
|
||||||
this.key = key;
|
this.key = key;
|
||||||
Cipher cipher = provider == null ? Cipher.getInstance(key.getAlgorithm()) : Cipher.getInstance(key.getAlgorithm(), provider);
|
Cipher cipher = provider == null ? Cipher.getInstance(key.getAlgorithm()) : Cipher.getInstance(key.getAlgorithm(), provider);
|
||||||
|
if (this.logger.isDebugEnabled())
|
||||||
|
this.logger.debug("cipher transformation: " + cipher.getAlgorithm());
|
||||||
|
|
||||||
// if the algorithm requires a stream cipher
|
// if the algorithm requires a stream cipher
|
||||||
if (cipher.getBlockSize() == 0) {
|
if (cipher.getBlockSize() == 0) {
|
||||||
@@ -54,8 +66,11 @@ public class AbstractCryptoByteChannel {
|
|||||||
Matcher matcher = ALGORITHM_PATTERN.matcher(cipher.getAlgorithm());
|
Matcher matcher = ALGORITHM_PATTERN.matcher(cipher.getAlgorithm());
|
||||||
if (!matcher.matches()) {
|
if (!matcher.matches()) {
|
||||||
if (cipher.getAlgorithm().contains("/"))
|
if (cipher.getAlgorithm().contains("/"))
|
||||||
throw new IllegalArgumentException("The algorithm contains a slash, but should contain 2 slashes, separating the algorithm, mode, and padding: " + cipher.getAlgorithm());
|
throw new IllegalArgumentException("The transformation contains a slash, but should contain 2 slashes, separating the algorithm, mode, and padding: " + cipher.getAlgorithm());
|
||||||
String algorithm = key.getAlgorithm() + "/" + DEFAULT_CIPHER_MODE + "/" + DEFAULT_CIPHER_PADDING;
|
String algorithm = key.getAlgorithm() + "/" + DEFAULT_CIPHER_MODE + "/" + DEFAULT_CIPHER_PADDING;
|
||||||
|
if (this.logger.isDebugEnabled())
|
||||||
|
this.logger.debug("defaulting to transformation: " + cipher.getAlgorithm());
|
||||||
|
|
||||||
matcher = ALGORITHM_PATTERN.matcher(algorithm);
|
matcher = ALGORITHM_PATTERN.matcher(algorithm);
|
||||||
if (!matcher.matches())
|
if (!matcher.matches())
|
||||||
throw new IllegalArgumentException("The algorithm is not formatted properly: " + algorithm);
|
throw new IllegalArgumentException("The algorithm is not formatted properly: " + algorithm);
|
||||||
@@ -64,12 +79,18 @@ public class AbstractCryptoByteChannel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (initializationVector != null) {
|
if (initializationVector != null) {
|
||||||
|
if (this.logger.isDebugEnabled())
|
||||||
|
this.logger.debug("initializing cipher with specified IV scrambler");
|
||||||
cipher.init(cipherMode, key, new IvParameterSpec(initializationVector));
|
cipher.init(cipherMode, key, new IvParameterSpec(initializationVector));
|
||||||
} else if (!matcher.group(2).equals("ECB")) {
|
} else if (!matcher.group(2).equals("ECB")) {
|
||||||
|
if (this.logger.isDebugEnabled())
|
||||||
|
this.logger.debug("initializing cipher with random IV scrambler");
|
||||||
byte[] iv = new byte[cipher.getBlockSize()];
|
byte[] iv = new byte[cipher.getBlockSize()];
|
||||||
new SecureRandom().nextBytes(iv);
|
new SecureRandom().nextBytes(iv);
|
||||||
cipher.init(cipherMode, key, new IvParameterSpec(iv));
|
cipher.init(cipherMode, key, new IvParameterSpec(iv));
|
||||||
} else {
|
} else {
|
||||||
|
if (this.logger.isDebugEnabled())
|
||||||
|
this.logger.debug("initializing cipher without IV scrambler");
|
||||||
cipher.init(cipherMode, key);
|
cipher.init(cipherMode, key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,9 +108,11 @@ public class AbstractCryptoByteChannel {
|
|||||||
|
|
||||||
public void setInitializationVector(byte[] initializationVector) throws InvalidAlgorithmParameterException {
|
public void setInitializationVector(byte[] initializationVector) throws InvalidAlgorithmParameterException {
|
||||||
try {
|
try {
|
||||||
|
if (this.logger.isDebugEnabled())
|
||||||
|
this.logger.debug("re-initializing cipher with specified IV scrambler");
|
||||||
this.cipher.init(this.cipherMode, this.key, new IvParameterSpec(initializationVector));
|
this.cipher.init(this.cipherMode, this.key, new IvParameterSpec(initializationVector));
|
||||||
} catch (InvalidKeyException ike) {
|
} catch (InvalidKeyException ike) {
|
||||||
throw new RuntimeException("This will never happen, as the key was already declared valid by previous init()");
|
throw new RuntimeException("This will never happen; the key was already declared valid by previous init()");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -21,10 +21,22 @@ import javax.crypto.ShortBufferException;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class implements a decrypting ByteChannel for the Java NIO framework.
|
||||||
|
*
|
||||||
|
* This class supports the use of an initialization vector (IV) to decipher
|
||||||
|
* content that was scrambled using an IV. An IV is effectively a second
|
||||||
|
* private, shared, or secret key. It is standard practice to embed the IV
|
||||||
|
* into the content as the first block. To automatically process those cases,
|
||||||
|
* use the IVDecryptingByteChannel instead of this class in this library.
|
||||||
|
*
|
||||||
|
* @author brian@inteligr8.com
|
||||||
|
*/
|
||||||
public class DecryptingByteChannel extends AbstractCryptoByteChannel implements ReadableByteChannel, Flushable {
|
public class DecryptingByteChannel extends AbstractCryptoByteChannel implements ReadableByteChannel, Flushable {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(DecryptingByteChannel.class);
|
private final Logger logger = LoggerFactory.getLogger(DecryptingByteChannel.class);
|
||||||
private final ReadableByteChannel rbchannel;
|
private final ReadableByteChannel rbchannel;
|
||||||
|
|
||||||
private boolean finished;
|
private boolean finished;
|
||||||
private boolean started;
|
private boolean started;
|
||||||
private boolean closed;
|
private boolean closed;
|
||||||
@@ -66,63 +78,141 @@ public class DecryptingByteChannel extends AbstractCryptoByteChannel implements
|
|||||||
this.started = false;
|
this.started = false;
|
||||||
|
|
||||||
if (this.getCipher().getBlockSize() > 0)
|
if (this.getCipher().getBlockSize() > 0)
|
||||||
|
// blockBuffer needs to hold at least 2 blocks, so just going with 3
|
||||||
|
// a block for a 256-bit key is 32 bytes
|
||||||
this.blockBuffer = ByteBuffer.allocate(this.getCipher().getBlockSize() * 3);
|
this.blockBuffer = ByteBuffer.allocate(this.getCipher().getBlockSize() * 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of bytes read before decryption. The same as the number of
|
||||||
|
* bytes read from the underlying ByteChannel.
|
||||||
|
*
|
||||||
|
* When using an embedded IV, this number includes those bytes. This also
|
||||||
|
* includes the padding bytes, so fully read block ciphers should result in
|
||||||
|
* numbers divisible by the block size.
|
||||||
|
*
|
||||||
|
* @return A number of bytes; 0 if not yet read; never negative
|
||||||
|
*/
|
||||||
public long getEncryptedBytesRead() {
|
public long getEncryptedBytesRead() {
|
||||||
return this.encryptedBytesRead;
|
return this.encryptedBytesRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of bytes read after decryption. The same as the number of
|
||||||
|
* bytes read from this ByteChannel.
|
||||||
|
*
|
||||||
|
* When using an embedded IV, this number includes those bytes. This does
|
||||||
|
* not include padding bytes. This value should be at most the result of
|
||||||
|
* `getEncryptedBytesRead()`.
|
||||||
|
*
|
||||||
|
* @return A number of bytes; 0 if not yet read; never negative
|
||||||
|
*/
|
||||||
public long getDecryptedBytesWritten() {
|
public long getDecryptedBytesWritten() {
|
||||||
return this.decryptedBytesWritten;
|
return this.decryptedBytesWritten;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close() throws IOException {
|
/**
|
||||||
|
* This method marks this ByteChannel as closed. It does not close the
|
||||||
|
* enclosed ByteChannel.
|
||||||
|
*
|
||||||
|
* This method does not close the underlying ByteChannel so you can have
|
||||||
|
* selectively crypto content embedded into a stream.
|
||||||
|
*/
|
||||||
|
public void close() {
|
||||||
// do not close wbchannel; we may be only encrypting some of the channel
|
// do not close wbchannel; we may be only encrypting some of the channel
|
||||||
if (!this.closed && this.started && !this.finished)
|
if (!this.closed && this.started && !this.finished)
|
||||||
this.logger.warn("A decryption channel was closed before it was properly finished");
|
this.logger.warn("A decryption channel was closed before it was properly finished");
|
||||||
this.closed = true;
|
this.closed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This ByteChannel is open. If the underlying ByteChannel is not open,
|
||||||
|
* this ByteChannel is considered closed.
|
||||||
|
*
|
||||||
|
* @return true if open; false if closed; default state is open
|
||||||
|
*/
|
||||||
public boolean isOpen() {
|
public boolean isOpen() {
|
||||||
return !this.closed && this.rbchannel.isOpen();
|
return !this.closed && this.rbchannel.isOpen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method does nothing but provide a warning if used earlier than expected.
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void flush() throws IOException {
|
public void flush() {
|
||||||
if (!this.closed && this.started && !this.finished && this.logger.isInfoEnabled())
|
if (!this.closed && this.started && !this.finished && this.logger.isInfoEnabled())
|
||||||
this.logger.info("A decryption channel was flushed before it was properly finished");
|
this.logger.info("A decryption channel was flushed before it was properly finished");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method sparks the reading of the underlying ByteChannel, decrypting
|
||||||
|
* the bytes in flight, and writing those decrypted bytes into the
|
||||||
|
* specified ByteBuffer.
|
||||||
|
*
|
||||||
|
* If there are no bytes to be read from the underlying ByteChannel, this
|
||||||
|
* method will do nothing and return -1. If there are bytes, those bytes
|
||||||
|
* could just be padding, so it will do something, but leave the specified
|
||||||
|
* ByteBuffer unchanged and return 0.
|
||||||
|
*
|
||||||
|
* After this method executes, there could be more bytes read from the
|
||||||
|
* underlying ByteChannel than have been written to specified ByteBuffer.
|
||||||
|
* This is because this class performs some buffering, which is necessary
|
||||||
|
* for block ciphers. That also means a call to this method may even leave
|
||||||
|
* the underlying ByteChannel untouched. Basically, do not expect the
|
||||||
|
* bytes read from the underlying ByteChannel to be sync with the bytes
|
||||||
|
* read from this ByteChannel.
|
||||||
|
*
|
||||||
|
* @param decryptedBuffer A NIO buffer ready for writing
|
||||||
|
* @return the number of bytes read from the underlying ByteChannel; -1 for EOF; could return 0 multiple times in a row
|
||||||
|
* @throws IOException An I/O exception occurred in the underlying ByteChannel
|
||||||
|
*/
|
||||||
public int read(ByteBuffer decryptedBuffer) throws IOException {
|
public int read(ByteBuffer decryptedBuffer) throws IOException {
|
||||||
if (this.closed)
|
if (this.closed)
|
||||||
throw new ClosedChannelException();
|
throw new ClosedChannelException();
|
||||||
|
|
||||||
if (this.logger.isTraceEnabled())
|
if (this.logger.isTraceEnabled())
|
||||||
this.logger.trace("write(" + decryptedBuffer.remaining() + "): ");
|
this.logger.trace("write(" + decryptedBuffer.remaining() + ")");
|
||||||
int totalBytesRead = 0;
|
int totalBytesRead = 0;
|
||||||
|
|
||||||
// if the channel is empty and cipher finalized
|
// if the channel is empty and cipher finalized
|
||||||
if (this.finished) {
|
if (this.finished) {
|
||||||
|
if (this.logger.isTraceEnabled())
|
||||||
|
this.logger.trace("write(" + decryptedBuffer.remaining() + "): finished reading underlying channel");
|
||||||
|
|
||||||
// and block buffer has some data still
|
// and block buffer has some data still
|
||||||
if (this.blockBuffer != null && this.blockBuffer.position() > 0) {
|
if (this.blockBuffer != null && this.blockBuffer.position() > 0) {
|
||||||
|
if (this.logger.isTraceEnabled())
|
||||||
|
this.logger.trace("write(" + decryptedBuffer.remaining() + "): draining buffer");
|
||||||
|
|
||||||
int bytesWritten = this.drainBlockBuffer(decryptedBuffer);
|
int bytesWritten = this.drainBlockBuffer(decryptedBuffer);
|
||||||
this.decryptedBytesWritten += bytesWritten;
|
this.decryptedBytesWritten += bytesWritten;
|
||||||
|
// all buffer work; nothing was read from the underlying channel
|
||||||
return 0;
|
return 0;
|
||||||
// otherwise no more data anywhere
|
// otherwise no more data anywhere
|
||||||
} else {
|
} else {
|
||||||
|
if (this.logger.isTraceEnabled())
|
||||||
|
this.logger.trace("write(" + decryptedBuffer.remaining() + "): nothing in buffer either; EOF");
|
||||||
|
// completely read, no buffer; done; EOF
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if block buffer has some data
|
// if block buffer has some data, start there
|
||||||
if (this.blockBuffer != null && this.blockBuffer.position() > 0) {
|
if (this.blockBuffer != null && this.blockBuffer.position() > 0) {
|
||||||
|
if (this.logger.isTraceEnabled())
|
||||||
|
this.logger.trace("write(" + decryptedBuffer.remaining() + "): draining buffer");
|
||||||
|
|
||||||
int bytesWritten = this.drainBlockBuffer(decryptedBuffer);
|
int bytesWritten = this.drainBlockBuffer(decryptedBuffer);
|
||||||
this.decryptedBytesWritten += bytesWritten;
|
this.decryptedBytesWritten += bytesWritten;
|
||||||
|
|
||||||
// if we filled the buffer, then we are done for now
|
// if we filled the buffer, then we are done for now
|
||||||
if (decryptedBuffer.remaining() == 0)
|
if (decryptedBuffer.remaining() == 0) {
|
||||||
|
if (this.logger.isTraceEnabled())
|
||||||
|
this.logger.trace("write(" + decryptedBuffer.remaining() + "): block buffer filled the specified buffer");
|
||||||
|
|
||||||
|
// all buffer work; nothing was read from the underlying channel, even if some bytes were ready
|
||||||
return 0;
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// assert: at this point, blockBuffer should be in clear position
|
// assert: at this point, blockBuffer should be in clear position
|
||||||
if (this.blockBuffer.position() > 0 || this.blockBuffer.limit() < this.blockBuffer.capacity())
|
if (this.blockBuffer.position() > 0 || this.blockBuffer.limit() < this.blockBuffer.capacity())
|
||||||
@@ -141,13 +231,20 @@ public class DecryptingByteChannel extends AbstractCryptoByteChannel implements
|
|||||||
totalBytesRead += Math.max(0, bytesRead);
|
totalBytesRead += Math.max(0, bytesRead);
|
||||||
} catch (BadPaddingException bpe) {
|
} catch (BadPaddingException bpe) {
|
||||||
throw new IOException("A padding issue occurred during decryption: " + bpe.getMessage(), bpe);
|
throw new IOException("A padding issue occurred during decryption: " + bpe.getMessage(), bpe);
|
||||||
} catch (IllegalBlockSizeException ibse) {
|
|
||||||
throw new IOException("A padding issue occurred during decryption: " + ibse.getMessage(), ibse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return totalBytesRead;
|
return totalBytesRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method copies every byte possible from one buffer to another. It
|
||||||
|
* stops copying gracefully when either the source buffer is empty or the
|
||||||
|
* target buffer is full.
|
||||||
|
*
|
||||||
|
* @param sourceBuffer A NIO buffer ready for reading
|
||||||
|
* @param targetBuffer A NIO buffer ready for writing
|
||||||
|
* @return The number of bytes transferred
|
||||||
|
*/
|
||||||
private int copyBuffer(ByteBuffer sourceBuffer, ByteBuffer targetBuffer) {
|
private int copyBuffer(ByteBuffer sourceBuffer, ByteBuffer targetBuffer) {
|
||||||
int bytesToCopy = Math.min(sourceBuffer.remaining(), targetBuffer.remaining());
|
int bytesToCopy = Math.min(sourceBuffer.remaining(), targetBuffer.remaining());
|
||||||
int realLimit = sourceBuffer.limit();
|
int realLimit = sourceBuffer.limit();
|
||||||
@@ -165,6 +262,14 @@ public class DecryptingByteChannel extends AbstractCryptoByteChannel implements
|
|||||||
return bytesToCopy;
|
return bytesToCopy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method drains the internal block buffer into the specified buffer.
|
||||||
|
* It will stop gracefully when the block buffer is empty or the specified
|
||||||
|
* buffer is full.
|
||||||
|
*
|
||||||
|
* @param decryptedBuffer A NIO buffer ready for writing
|
||||||
|
* @return The number of bytes transferred
|
||||||
|
*/
|
||||||
private int drainBlockBuffer(ByteBuffer decryptedBuffer) {
|
private int drainBlockBuffer(ByteBuffer decryptedBuffer) {
|
||||||
// switch to read mode
|
// switch to read mode
|
||||||
this.blockBuffer.flip();
|
this.blockBuffer.flip();
|
||||||
@@ -178,6 +283,14 @@ public class DecryptingByteChannel extends AbstractCryptoByteChannel implements
|
|||||||
return bytesCopied;
|
return bytesCopied;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method reads the underlying ByteChannel into the specified
|
||||||
|
* ByteBuffer.
|
||||||
|
*
|
||||||
|
* @param buffer A NIO buffer ready for writing
|
||||||
|
* @return The number of bytes read and written; -1 for EOF
|
||||||
|
* @throws IOException An I/O exception occurred in the underlying ByteChannel
|
||||||
|
*/
|
||||||
private int readBuffer(ByteBuffer buffer) throws IOException {
|
private int readBuffer(ByteBuffer buffer) throws IOException {
|
||||||
int totalBytesRead = 0;
|
int totalBytesRead = 0;
|
||||||
int bytesRead = this.rbchannel.read(buffer);
|
int bytesRead = this.rbchannel.read(buffer);
|
||||||
@@ -194,13 +307,25 @@ public class DecryptingByteChannel extends AbstractCryptoByteChannel implements
|
|||||||
return totalBytesRead;
|
return totalBytesRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int decryptToBuffer(ByteBuffer decryptedBuffer) throws BadPaddingException, IllegalBlockSizeException, IOException {
|
/**
|
||||||
|
* This method reads from the underlying ByteChannel, decrypts the content,
|
||||||
|
* and writes it to the specified ByteBuffer. Any excess read bytes are
|
||||||
|
* stored in a block buffer.
|
||||||
|
*
|
||||||
|
* @param decryptedBuffer A NIO buffer ready for writing
|
||||||
|
* @return The number of bytes read from the underlying ByteChannel; could be 0 multiple times in a row
|
||||||
|
* @throws BadPaddingException The content was not padded as expected
|
||||||
|
* @throws IOException An I/O exception occurred in the underlying ByteChannel
|
||||||
|
*/
|
||||||
|
private int decryptToBuffer(ByteBuffer decryptedBuffer) throws BadPaddingException, IOException {
|
||||||
int blockSize = this.getCipher().getBlockSize();
|
int blockSize = this.getCipher().getBlockSize();
|
||||||
|
|
||||||
ByteBuffer encryptedBuffer = null;
|
ByteBuffer encryptedBuffer = null;
|
||||||
if (this.blockBuffer == null) {
|
if (this.blockBuffer == null) {
|
||||||
|
// for streaming ciphers, create a buffer the same size as the specified ByteBuffer
|
||||||
encryptedBuffer = ByteBuffer.allocate(decryptedBuffer.remaining());
|
encryptedBuffer = ByteBuffer.allocate(decryptedBuffer.remaining());
|
||||||
} else {
|
} else {
|
||||||
|
// for block ciphers, round up to the next size divisible by the block size, plus one more block for extra buffering
|
||||||
int encryptedBufferSize = decryptedBuffer.remaining() / blockSize * blockSize + blockSize;
|
int encryptedBufferSize = decryptedBuffer.remaining() / blockSize * blockSize + blockSize;
|
||||||
encryptedBuffer = ByteBuffer.allocate(encryptedBufferSize);
|
encryptedBuffer = ByteBuffer.allocate(encryptedBufferSize);
|
||||||
}
|
}
|
||||||
@@ -209,16 +334,16 @@ public class DecryptingByteChannel extends AbstractCryptoByteChannel implements
|
|||||||
if (this.logger.isTraceEnabled())
|
if (this.logger.isTraceEnabled())
|
||||||
this.logger.trace("decryptToBuffer(): max bytes to read: " + maxBytesToRead);
|
this.logger.trace("decryptToBuffer(): max bytes to read: " + maxBytesToRead);
|
||||||
|
|
||||||
// read encrypted bytes from channel
|
// read encrypted bytes from the underlying channel
|
||||||
int bytesRead = this.readBuffer(encryptedBuffer);
|
int bytesRead = this.readBuffer(encryptedBuffer);
|
||||||
if (this.logger.isTraceEnabled())
|
if (this.logger.isTraceEnabled())
|
||||||
this.logger.trace("decryptToBuffer(): bytes actually read: " + bytesRead);
|
this.logger.trace("decryptToBuffer(): bytes actually read: " + bytesRead);
|
||||||
|
|
||||||
// if nothing more to read...ever
|
// if nothing more to read...ever (EOF)
|
||||||
if (bytesRead < 0) {
|
if (bytesRead < 0) {
|
||||||
try {
|
try {
|
||||||
// try and finish the decryption
|
// try and finish the decryption
|
||||||
// since the buffer may not be big enough, use the block buffer
|
// since the encrypted buffer may not be big enough, use the block buffer
|
||||||
int bytesWritten = this.getCipher().doFinal(ByteBuffer.allocate(0), this.blockBuffer);
|
int bytesWritten = this.getCipher().doFinal(ByteBuffer.allocate(0), this.blockBuffer);
|
||||||
if (this.logger.isTraceEnabled()) {
|
if (this.logger.isTraceEnabled()) {
|
||||||
this.logger.trace("decryptToBuffer(): bytes actually decrypted: 0");
|
this.logger.trace("decryptToBuffer(): bytes actually decrypted: 0");
|
||||||
@@ -232,6 +357,9 @@ public class DecryptingByteChannel extends AbstractCryptoByteChannel implements
|
|||||||
this.finished = true;
|
this.finished = true;
|
||||||
|
|
||||||
return bytesWritten > 0 ? 0 : -1;
|
return bytesWritten > 0 ? 0 : -1;
|
||||||
|
} catch (IllegalBlockSizeException ibse) {
|
||||||
|
// this exception is reserved for encryption only; not decryption
|
||||||
|
throw new RuntimeException("This should never happen", ibse);
|
||||||
} catch (ShortBufferException sbe) {
|
} catch (ShortBufferException sbe) {
|
||||||
throw new BufferOverflowException();
|
throw new BufferOverflowException();
|
||||||
}
|
}
|
||||||
|
@@ -21,9 +21,15 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No need for block buffer?
|
* This class implements an encrypting ByteChannel for the Java NIO framework.
|
||||||
*
|
*
|
||||||
* @author brian.long
|
* This class supports the specification of an initialization vector (IV) to
|
||||||
|
* scramble content using an IV. An IV is effectively a second private,
|
||||||
|
* shared, or secret key. It is standard practice to embed the IV into the
|
||||||
|
* content as the first block. To automatically do that, use the
|
||||||
|
* IVEncryptingByteChannel instead of this class in this library.
|
||||||
|
*
|
||||||
|
* @author brian@ingeligr8.com
|
||||||
*/
|
*/
|
||||||
public class EncryptingByteChannel extends AbstractCryptoByteChannel implements WritableByteChannel, Flushable {
|
public class EncryptingByteChannel extends AbstractCryptoByteChannel implements WritableByteChannel, Flushable {
|
||||||
|
|
||||||
@@ -62,40 +68,98 @@ public class EncryptingByteChannel extends AbstractCryptoByteChannel implements
|
|||||||
this.wbchannel = wbchannel;
|
this.wbchannel = wbchannel;
|
||||||
this.closed = !wbchannel.isOpen();
|
this.closed = !wbchannel.isOpen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of bytes read before encryption. The same as the number of
|
||||||
|
* bytes read from the underlying ByteChannel.
|
||||||
|
*
|
||||||
|
* When using an embedded IV, this number includes those bytes. This does
|
||||||
|
* does not include the padding bytes. The value should be at most the
|
||||||
|
* value of the 'getEncryptedBytesWritten()' when finished.
|
||||||
|
*
|
||||||
|
* @return A number of bytes; 0 if not yet read; never negative
|
||||||
|
*/
|
||||||
public long getDecryptedBytesRead() {
|
public long getDecryptedBytesRead() {
|
||||||
return this.decryptedBytesRead;
|
return this.decryptedBytesRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of bytes read after encryption. The same as the number of
|
||||||
|
* bytes read from this ByteChannel.
|
||||||
|
*
|
||||||
|
* When using an embedded IV, this number includes those bytes. This also
|
||||||
|
* includes the padding bytes, so fully read block ciphers should result in
|
||||||
|
* numbers divisible by the block size.
|
||||||
|
*
|
||||||
|
* @return A number of bytes; 0 if not yet read; never negative
|
||||||
|
*/
|
||||||
public long getEncryptedBytesWritten() {
|
public long getEncryptedBytesWritten() {
|
||||||
return this.encryptedBytesWritten;
|
return this.encryptedBytesWritten;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method flushes and then marks this ByteChannel as closed. It does
|
||||||
|
* not close the enclosed ByteChannel.
|
||||||
|
*
|
||||||
|
* This method does not close the underlying ByteChannel so you can have
|
||||||
|
* selectively crypto content embedded into a stream.
|
||||||
|
*/
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
// do not close wbchannel; we may be only encrypting some of the channel
|
// do not close wbchannel; we may be only encrypting some of the channel
|
||||||
if (!this.closed)
|
if (!this.closed)
|
||||||
this.flush();
|
this.flush();
|
||||||
this.closed = true;
|
this.closed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This ByteChannel is open. If the underlying ByteChannel is not open,
|
||||||
|
* this ByteChannel is considered closed.
|
||||||
|
*
|
||||||
|
* @return true if open; false if closed; default state is open
|
||||||
|
*/
|
||||||
public boolean isOpen() {
|
public boolean isOpen() {
|
||||||
return !this.closed && this.wbchannel.isOpen();
|
return !this.closed && this.wbchannel.isOpen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method finalizes the encryption, padding it appropriately, writing
|
||||||
|
* the final bytes to the block buffer.
|
||||||
|
*
|
||||||
|
* @throws IOException An I/O exception occurred
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void flush() throws IOException {
|
public void flush() throws IOException {
|
||||||
if (!this.finished) {
|
if (!this.finished) {
|
||||||
try {
|
try {
|
||||||
this.encryptFinish();
|
this.encryptFinish();
|
||||||
} catch (IllegalBlockSizeException ibse) {
|
} catch (IllegalBlockSizeException ibse) {
|
||||||
// output error, because a lot of people like to supress IOException on close
|
// output error, because a lot of people like to suppress IOException on close
|
||||||
String message = "The encryption algorithm is a block algorithm which requires padding: " + ibse.getMessage();
|
String message = "The encryption algorithm is a block algorithm which requires padding: " + ibse.getMessage();
|
||||||
this.logger.error(message);
|
this.logger.error(message);
|
||||||
throw new IOException(message, ibse);
|
throw new IOException(message, ibse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method sparks the reading of the underlying ByteChannel, encrypting
|
||||||
|
* the bytes in flight, and writing those encrypted bytes into the
|
||||||
|
* specified ByteBuffer.
|
||||||
|
*
|
||||||
|
* If there are no bytes to be read from the underlying ByteChannel, this
|
||||||
|
* method will return 0 or -1. If there is nothing in the block buffer,
|
||||||
|
* it would return -1.
|
||||||
|
*
|
||||||
|
* After this method executes, there could be more bytes read from the
|
||||||
|
* underlying ByteChannel than have been written to specified ByteBuffer.
|
||||||
|
* This is because this class performs some buffering, which is necessary
|
||||||
|
* for block ciphers. That also means a call to this method may even leave
|
||||||
|
* the underlying ByteChannel untouched. Basically, do not expect the
|
||||||
|
* bytes read from the underlying ByteChannel to be sync with the bytes
|
||||||
|
* read from this ByteChannel.
|
||||||
|
*
|
||||||
|
* @return the number of bytes written to the underlying ByteChannel
|
||||||
|
*/
|
||||||
public int write(ByteBuffer decryptedBuffer) throws IOException {
|
public int write(ByteBuffer decryptedBuffer) throws IOException {
|
||||||
if (this.logger.isTraceEnabled())
|
if (this.logger.isTraceEnabled())
|
||||||
this.logger.trace("write(" + decryptedBuffer.remaining() + "): ");
|
this.logger.trace("write(" + decryptedBuffer.remaining() + "): ");
|
||||||
|
@@ -12,6 +12,13 @@ import java.security.Provider;
|
|||||||
|
|
||||||
import javax.crypto.NoSuchPaddingException;
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class assumes a random initialization vector at the start of the
|
||||||
|
* encrypted content. Otherwise it acts identical to the
|
||||||
|
* DecryptingByteChannel.
|
||||||
|
*
|
||||||
|
* @author brian@inteligr8.com
|
||||||
|
*/
|
||||||
public class IVDecryptingByteChannel implements ReadableByteChannel, Flushable {
|
public class IVDecryptingByteChannel implements ReadableByteChannel, Flushable {
|
||||||
|
|
||||||
private final ReadableByteChannel rbchannel;
|
private final ReadableByteChannel rbchannel;
|
||||||
|
@@ -12,6 +12,13 @@ import java.security.Provider;
|
|||||||
|
|
||||||
import javax.crypto.NoSuchPaddingException;
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class embeds a random initialization vector at the start of the
|
||||||
|
* encrypted content. Otherwise it acts identical to the
|
||||||
|
* EncryptingByteChannel.
|
||||||
|
*
|
||||||
|
* @author brian@inteligr8.com
|
||||||
|
*/
|
||||||
public class IVEncryptingByteChannel implements WritableByteChannel, Flushable {
|
public class IVEncryptingByteChannel implements WritableByteChannel, Flushable {
|
||||||
|
|
||||||
private final EncryptingByteChannel ebchannel;
|
private final EncryptingByteChannel ebchannel;
|
||||||
|
32
src/test/java/com/inteligr8/nio/CommonSamples.java
Executable file
32
src/test/java/com/inteligr8/nio/CommonSamples.java
Executable file
@@ -0,0 +1,32 @@
|
|||||||
|
package com.inteligr8.nio;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.nio.channels.Channels;
|
||||||
|
import java.nio.channels.Pipe;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
|
import javax.crypto.KeyGenerator;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class CommonSamples {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aes() throws Exception {
|
||||||
|
// generate random key using JCE
|
||||||
|
int keysize = 256;
|
||||||
|
KeyGenerator keygen = KeyGenerator.getInstance("AES");
|
||||||
|
keygen.init(keysize);
|
||||||
|
SecretKey key = keygen.generateKey();
|
||||||
|
|
||||||
|
// pipe data
|
||||||
|
Pipe pipe = Pipe.open();
|
||||||
|
IVEncryptingByteChannel cryptochannel = new IVEncryptingByteChannel(pipe.sink(), key);
|
||||||
|
Writer writer = Channels.newWriter(cryptochannel, Charset.defaultCharset());
|
||||||
|
writer.append("some plain text to encrypt");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user