added initial DigestBuffer

This commit is contained in:
brian
2021-02-21 22:42:16 -05:00
parent 9928117f4d
commit 86d546fd21
8 changed files with 435 additions and 47 deletions

View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.inteligr8</groupId>
<artifactId>nio-crypto</artifactId>
<version>1.0-SNAPSHOT</version>
<version>2.0-SNAPSHOT</version>
<name>Java NIO Crypto Adapter</name>
<description>This project implements the javax.crypto API using java.nio instead of java.io.</description>

View File

@@ -0,0 +1,55 @@
package com.inteligr8.nio;
import java.nio.ByteBuffer;
/**
* @author brian@inteligr8.com
*/
public abstract class AbstractBuffer {
protected long sourceBytesRead = 0L;
protected long targetBytesWritten = 0L;
protected boolean started = false;
protected boolean finished = false;
public Status getStatus() {
if (this.finished) return Status.Finished;
else if (this.started) return Status.Started;
else return Status.Initialized;
}
public long getTotalBytesRead() {
return this.sourceBytesRead;
}
public long getTotalBytesWritten() {
return this.targetBytesWritten;
}
/**
* 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
*/
protected int copyBuffer(ByteBuffer sourceBuffer, ByteBuffer targetBuffer) {
int bytesToCopy = Math.min(sourceBuffer.remaining(), targetBuffer.remaining());
int realLimit = sourceBuffer.limit();
int tmpLimit = sourceBuffer.position() + bytesToCopy;
// read only the amount both buffers can handle
sourceBuffer.limit(tmpLimit);
// do the actual transfer of bytes
targetBuffer.put(sourceBuffer);
// reset the limit to its actual value (which could be the same)
sourceBuffer.limit(realLimit);
return bytesToCopy;
}
}

View File

@@ -20,7 +20,15 @@ import javax.crypto.spec.IvParameterSpec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CipherBuffer {
/**
* This class attempts to implement the JCE Cipher using the Java NIO library
* and concepts. The JCE Cipher accepts ByteBuffer, but it can be complicated
* to use. This class makes encryption and decryption act more like typical
* NIO buffer operations.
*
* @author brian@inteligr8.com
*/
public class CipherBuffer extends AbstractBuffer {
private final Logger logger = LoggerFactory.getLogger(CipherBuffer.class);
private final String DEFAULT_CIPHER_MODE = "ECB";
@@ -31,10 +39,6 @@ public class CipherBuffer {
private final Key key;
private final Cipher cipher;
private final ByteBuffer leftoverBuffer;
private long sourceBytesRead = 0L;
private long targetBytesWritten = 0L;
private boolean started = false;
private boolean finished = false;
public CipherBuffer(CipherParameters cparams)
throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
@@ -101,20 +105,6 @@ public class CipherBuffer {
public Cipher getCipher() {
return this.cipher;
}
public Status getStatus() {
if (this.finished) return Status.Finished;
else if (this.started) return Status.Started;
else return Status.Initialized;
}
public long getTotalBytesRead() {
return this.sourceBytesRead;
}
public long getTotalBytesWritten() {
return this.targetBytesWritten;
}
public byte[] getInitializationVector() {
return this.cipher.getIV();
@@ -135,7 +125,7 @@ public class CipherBuffer {
* specified buffer. Any subsequent call to `stream()` will fail.
*
* @param targetBuffer A NIO buffer ready for writing
* @return true if more data may need flushed; false if subsequent calls will do nothing
* @return true if more data may need flushed; false if subsequent calls are not expected
* @throws BadPaddingException
* @throws IllegalBlockSizeException
*/
@@ -282,31 +272,5 @@ public class CipherBuffer {
sourceBuffer.limit(limitBackup);
return oldTargetBytesWritten < this.targetBytesWritten || sourceBuffer.hasRemaining();
}
/**
* 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) {
int bytesToCopy = Math.min(sourceBuffer.remaining(), targetBuffer.remaining());
int realLimit = sourceBuffer.limit();
int tmpLimit = sourceBuffer.position() + bytesToCopy;
// read only the amount both buffers can handle
sourceBuffer.limit(tmpLimit);
// do the actual transfer of bytes
targetBuffer.put(sourceBuffer);
// reset the limit to its actual value (which could be the same)
sourceBuffer.limit(realLimit);
return bytesToCopy;
}
}

View File

@@ -0,0 +1,109 @@
package com.inteligr8.nio;
import java.nio.ByteBuffer;
import java.security.DigestException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class attempts to implement the JCA MessageDigest using the Java NIO
* library and concepts. It makes hashing act more like typical NIO buffer
* operations.
*
* @author brian@inteligr8.com
*/
public class DigestBuffer extends AbstractBuffer {
private final Logger logger = LoggerFactory.getLogger(DigestBuffer.class);
private final MessageDigest digest;
private final ByteBuffer leftoverBuffer;
public DigestBuffer(DigestParameters dparams) throws NoSuchAlgorithmException {
this.digest = dparams.getProvider() == null ? MessageDigest.getInstance(dparams.getAlgorithm()) : MessageDigest.getInstance(dparams.getAlgorithm(), dparams.getProvider());
if (this.logger.isDebugEnabled())
this.logger.debug("digest algorithm: " + this.digest.getAlgorithm());
this.leftoverBuffer = ByteBuffer.allocate(this.digest.getDigestLength());
this.leftoverBuffer.flip();
}
public MessageDigest getMessageDigest() {
return this.digest;
}
/**
* This method flushes the digest cache and buffer into the specified
* buffer. Any subsequent call to `stream()` will fail.
*
* @param targetBuffer A NIO buffer ready for writing
* @return true if more data may need flushed; false if subsequent calls will do nothing
*/
public boolean flush(ByteBuffer targetBuffer) throws DigestException {
if (this.logger.isTraceEnabled())
this.logger.trace("flush(" + targetBuffer.remaining() + ")");
long oldTargetBytesWritten = this.targetBytesWritten;
// leftovers exist; must eat
if (this.leftoverBuffer.remaining() > 0) {
if (this.logger.isTraceEnabled())
this.logger.trace("flush(" + targetBuffer.remaining() + "): draining leftovers");
this.targetBytesWritten += this.copyBuffer(this.leftoverBuffer, targetBuffer);
if (this.leftoverBuffer.remaining() > 0)
return true;
// otherwise no more data anywhere
} else {
if (this.logger.isTraceEnabled())
this.logger.trace("flush(" + targetBuffer.remaining() + "): nothing to flush");
}
if (!this.finished) {
// ready for writing
this.leftoverBuffer.clear();
byte[] bytes = this.digest.digest();
this.leftoverBuffer.put(bytes);
this.finished = true;
// ready for reading
this.leftoverBuffer.flip();
this.targetBytesWritten += this.copyBuffer(this.leftoverBuffer, targetBuffer);
if (this.leftoverBuffer.hasRemaining())
return true;
}
return oldTargetBytesWritten < this.targetBytesWritten;
}
/**
* This method streams the digest from a source NIO buffer to a target NIO
* buffer.
*
* @param sourceBuffer A NIO buffer ready for reading
*/
public void stream(ByteBuffer sourceBuffer) {
if (sourceBuffer == null)
throw new IllegalArgumentException();
if (this.finished)
throw new IllegalStateException("There should no longer be a source to this digest");
if (this.logger.isTraceEnabled())
this.logger.trace("stream(" + sourceBuffer.remaining() + ")");
if (!this.started)
this.started = true;
int bytesToRead = sourceBuffer.remaining();
this.digest.update(sourceBuffer);
this.sourceBytesRead += bytesToRead - sourceBuffer.remaining();
}
}

View File

@@ -0,0 +1,36 @@
package com.inteligr8.nio;
import java.security.Provider;
public class DigestParameters {
private String algorithm;
private Provider provider;
public DigestParameters(String algorithm) {
this(algorithm, null);
}
public DigestParameters(String algorithm, Provider provider) {
this.algorithm = algorithm;
this.provider = provider;
}
public String getAlgorithm() {
return this.algorithm;
}
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
public Provider getProvider() {
return this.provider;
}
public DigestParameters setProvider(Provider provider) {
this.provider = provider;
return this;
}
}

View File

@@ -0,0 +1,166 @@
package com.inteligr8.nio;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.security.DigestException;
import java.security.NoSuchAlgorithmException;
import org.apache.commons.codec.binary.Hex;
import org.junit.Assert;
import org.junit.Test;
public abstract class AbstractDigestUnitTest {
public abstract String getDefaultAlgorithm();
protected DigestBuffer createDigest() throws NoSuchAlgorithmException {
return new DigestBuffer(new DigestParameters(this.getDefaultAlgorithm()));
}
public void validateText(String text, String hex) throws NoSuchAlgorithmException, DigestException {
ByteBuffer buffer = ByteBuffer.wrap(text.getBytes(Charset.forName("utf-8")));
DigestBuffer digest = this.createDigest();
digest.stream(buffer);
Assert.assertFalse(buffer.hasRemaining());
ByteBuffer output = ByteBuffer.allocate(digest.getMessageDigest().getDigestLength());
while (digest.flush(output))
;
output.flip();
Assert.assertEquals(hex, this.buffer2hex(output));
}
public void validateFile(File file, String hex) throws NoSuchAlgorithmException, IOException, DigestException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
DigestBuffer digest = this.createDigest();
FileChannel fchannel = FileChannel.open(file.toPath());
try {
while (fchannel.read(buffer) >= 0) {
buffer.flip();
digest.stream(buffer);
buffer.compact();
}
} finally {
fchannel.close();
}
ByteBuffer output = ByteBuffer.allocate(digest.getMessageDigest().getDigestLength());
while (digest.flush(output))
;
output.flip();
Assert.assertEquals(hex, this.buffer2hex(output));
}
private String buffer2hex(ByteBuffer buffer) {
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
return new String(Hex.encodeHex(bytes));
}
@Test
public void testEmptyFlushEmptyTargetHash() throws Exception {
DigestBuffer digest = this.createDigest();
ByteBuffer buffer = ByteBuffer.allocate(0);
Assert.assertTrue(digest.flush(buffer));
Assert.assertEquals(0L, digest.getTotalBytesRead());
Assert.assertEquals(0L, digest.getTotalBytesWritten());
Assert.assertTrue(digest.flush(buffer));
}
@Test
public void testEmptyFlushHash() throws Exception {
DigestBuffer digest = this.createDigest();
ByteBuffer buffer = ByteBuffer.allocate(1024);
Assert.assertTrue(digest.flush(buffer));
Assert.assertEquals(0L, digest.getTotalBytesRead());
Assert.assertEquals(digest.getMessageDigest().getDigestLength(), digest.getTotalBytesWritten());
Assert.assertFalse(digest.flush(buffer));
}
@Test
public void testEmptyMultiFlushHash() throws Exception {
DigestBuffer digest = this.createDigest();
int chunkSize = digest.getMessageDigest().getDigestLength() / 2 - 1;
ByteBuffer buffer = ByteBuffer.allocate(chunkSize);
Assert.assertTrue(digest.flush(buffer));
Assert.assertEquals(0L, digest.getTotalBytesRead());
Assert.assertEquals(chunkSize, digest.getTotalBytesWritten());
Assert.assertEquals(chunkSize, buffer.position());
buffer.clear();
Assert.assertTrue(digest.flush(buffer));
Assert.assertEquals(0L, digest.getTotalBytesRead());
Assert.assertEquals(chunkSize * 2L, digest.getTotalBytesWritten());
Assert.assertEquals(chunkSize, buffer.position());
buffer.clear();
Assert.assertTrue(digest.flush(buffer));
Assert.assertEquals(0L, digest.getTotalBytesRead());
Assert.assertEquals(digest.getMessageDigest().getDigestLength(), digest.getTotalBytesWritten());
Assert.assertEquals(2, buffer.position());
buffer.clear();
Assert.assertFalse(digest.flush(buffer));
Assert.assertEquals(0L, digest.getTotalBytesRead());
Assert.assertEquals(digest.getMessageDigest().getDigestLength(), digest.getTotalBytesWritten());
Assert.assertEquals(0, buffer.position());
Assert.assertFalse(digest.flush(buffer));
}
@Test
public void testEmptyStreamEmptyTargetHash() throws Exception {
DigestBuffer digest = this.createDigest();
ByteBuffer sourceBuffer = ByteBuffer.allocate(0);
digest.stream(sourceBuffer);
Assert.assertEquals(0L, digest.getTotalBytesRead());
Assert.assertEquals(0L, digest.getTotalBytesWritten());
}
@Test
public void testEmptyStreamHash() throws Exception {
DigestBuffer digest = this.createDigest();
ByteBuffer sourceBuffer = ByteBuffer.allocate(0);
digest.stream(sourceBuffer);
Assert.assertEquals(0L, digest.getTotalBytesRead());
Assert.assertEquals(0L, digest.getTotalBytesWritten());
}
@Test
public void testEmptyMultiStreamHash() throws Exception {
DigestBuffer digest = this.createDigest();
ByteBuffer sourceBuffer = ByteBuffer.allocate(0);
digest.stream(sourceBuffer);
Assert.assertEquals(0L, digest.getTotalBytesRead());
Assert.assertEquals(0L, digest.getTotalBytesWritten());
digest.stream(sourceBuffer);
}
@Test(expected = IllegalStateException.class)
public void testStreamAfterFlush() throws Exception {
DigestBuffer digest = this.createDigest();
ByteBuffer sourceBuffer = ByteBuffer.allocate(0);
ByteBuffer targetBuffer = ByteBuffer.allocate(0);
Assert.assertTrue(digest.flush(targetBuffer));
digest.stream(sourceBuffer);
Assert.fail();
}
}

View File

@@ -0,0 +1,29 @@
package com.inteligr8.nio;
import java.io.File;
import org.junit.Test;
public class Md5DigestUnitTest extends AbstractDigestUnitTest {
@Override
public String getDefaultAlgorithm() {
return "MD5";
}
@Test
public void shortText() throws Exception {
this.validateText("Hello", "8b1a9953c4611296a827abf8c47804d7");
}
@Test
public void longText() throws Exception {
this.validateText("Here is some text that is much longer than short. It is long. How about that?", "d9c042e20cc3215cda5f7f3648623abb");
}
@Test
public void fileText() throws Exception {
this.validateFile(new File("pom.xml"), "ba34d113dab4cc80d148ba6745852efd");
}
}

View File

@@ -0,0 +1,29 @@
package com.inteligr8.nio;
import java.io.File;
import org.junit.Test;
public class Sha256DigestUnitTest extends AbstractDigestUnitTest {
@Override
public String getDefaultAlgorithm() {
return "SHA-256";
}
@Test
public void shortText() throws Exception {
this.validateText("Hello", "185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969");
}
@Test
public void longText() throws Exception {
this.validateText("Here is some text that is much longer than short. It is long. How about that?", "3acef9887592d6f6347bf750d0b63d1d652ec81d88fe0ff2f5f1d7aa6f21dc29");
}
@Test
public void fileText() throws Exception {
this.validateFile(new File("pom.xml"), "f9b266659cb1137cda357ad6bf31509f13defbe53c0e5a90069bc9eb95387800");
}
}