From 97c06990c4cc8594e9cf23d6d0fc281b4b97f7a8 Mon Sep 17 00:00:00 2001 From: Patrick Favre-Bulle Date: Thu, 9 Aug 2018 16:43:07 +0200 Subject: [PATCH] Switch to Okio Base64 implementation #8 --- README.md | 2 +- .../lib/crypto/bcrypt/Radix64Encoder.java | 388 ++++-------------- .../favre/lib/crypto/bcrypt/Radix64Test.java | 14 +- 3 files changed, 96 insertions(+), 308 deletions(-) diff --git a/README.md b/README.md index 79e9f52..c86c83f 100644 --- a/README.md +++ b/README.md @@ -376,7 +376,7 @@ Use the Maven wrapper to create a jar including all dependencies ## Libraries & Credits * [jBcrypt](https://github.com/jeremyh/jBCrypt) (derived the "Blowfish Expensive key setup") (under BSD licence) -* Radix64 implementation derived from [Apache Commons Codec](https://commons.apache.org/codec/) (under Apache v2) +* Radix64 implementation derived from [Square's Okio Base64](https://github.com/square/okio) (under Apache v2) ### BCrypt Implementations in Java diff --git a/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/Radix64Encoder.java b/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/Radix64Encoder.java index e6f5531..1213df5 100644 --- a/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/Radix64Encoder.java +++ b/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/Radix64Encoder.java @@ -43,7 +43,7 @@ public interface Radix64Encoder { /** * Encode given raw byte array to a Radix64 style, UTF-8 encoded byte array. * - * @param rawBytes to encode + * @param rawBytes to encode * @return UTF-8 encoded string representing radix64 encoded data */ byte[] encode(byte[] rawBytes); @@ -57,35 +57,11 @@ public interface Radix64Encoder { byte[] decode(byte[] utf8EncodedRadix64String); /** - * A mod of the Apache Commons Codec Base64 logic + * A mod of Square's Okio Base64 encoder + * + * @see Okio */ class Default implements Radix64Encoder { - - private static final int BITS_PER_ENCODED_BYTE = 6; - private static final int BYTES_PER_UNENCODED_BLOCK = 3; - private static final int BYTES_PER_ENCODED_BLOCK = 4; - private static final int MASK_6BITS = 0x3f; - private static final int MASK_8BITS = 0xff; - private static final int DEFAULT_BUFFER_RESIZE_FACTOR = 2; - private static final int DEFAULT_BUFFER_SIZE = 8192; - - /** - * This array is a lookup table that translates 6-bit positive integer index values into their "Radix64ApacheCodec Alphabet" - * equivalents. - */ - private static final byte[] STANDARD_ENCODE_TABLE = { - '.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', - 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', - 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', - 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', - 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', - '6', '7', '8', '9' - }; - - /** - * This array is a lookup table that translates Unicode characters drawn from the "Radix64ApacheCodec Alphabet" into their 6-bit positive i - * integer equivalents. - */ private static final byte[] DECODE_TABLE = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, @@ -93,299 +69,109 @@ class Default implements Radix64Encoder { 58, 59, 60, 61, 62, 63, -1, -1, -1, -2, -1, -1, -1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, -1, -1, -1, -1, -1, -1, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, - 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1 - }; - - private final byte[] encodeTable; - private final int decodeSize; - private final int encodeSize; + 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53}; - /** - * Creates a Radix64ApacheCodec codec used for decoding (all modes) and encoding in URL-unsafe mode. - *

- * When encoding the line length and line separator are given in the constructor, and the encoding table is - * STANDARD_ENCODE_TABLE. - *

- *

- * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data. - *

- *

- * When decoding all variants are supported. - *

- * - * @throws IllegalArgumentException The provided lineSeparator included some base64 characters. That's not going to work! - * @since 1.4 - */ - public Default() { - this.encodeSize = BYTES_PER_ENCODED_BLOCK; - this.decodeSize = this.encodeSize - 1; - this.encodeTable = STANDARD_ENCODE_TABLE; - } + private static final byte[] MAP = new byte[]{ + '.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', + 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', + '6', '7', '8', '9' + }; @Override - public byte[] encode(byte[] rawBytes) { - final Context c = new Context(); - encode(rawBytes, 0, rawBytes.length, c); - encode(rawBytes, 0, -1, c); // Notify encoder of EOF. - final byte[] buf = new byte[c.pos - c.readPos]; - readResults(buf, 0, buf.length, c); - return buf; + public byte[] encode(byte[] in) { + return encode(in, MAP); } - /** - * Extracts buffered data into the provided byte[] array, starting at position bPos, up to a maximum of bAvail - * bytes. Returns how many bytes were actually extracted. - *

- * Package protected for access from I/O streams. - * - * @param b byte[] array to extract the buffered data into. - * @param bPos position in byte[] array to start extraction at. - * @param bAvail amount of bytes we're allowed to extract. We may extract fewer (if fewer are available). - * @param context the context to be used - */ - private void readResults(final byte[] b, final int bPos, final int bAvail, final Context context) { - if (context.buffer != null) { - final int len = Math.min(context.pos - context.readPos, bAvail); - System.arraycopy(context.buffer, context.readPos, b, bPos, len); - context.readPos += len; - if (context.readPos >= context.pos) { - context.buffer = null; // so hasData() will return false, and this method can return -1 + @Override + public byte[] decode(byte[] in) { + // Ignore trailing '=' padding and whitespace from the input. + int limit = in.length; + for (; limit > 0; limit--) { + byte c = in[limit - 1]; + if (c != '=' && c != '\n' && c != '\r' && c != ' ' && c != '\t') { + break; } } - } - /** - *

- * Encodes all of the provided data, starting at inPos, for inAvail bytes. Must be called at least twice: once with - * the data to encode, and once with inAvail set to "-1" to alert encoder that EOF has been reached, to flush last - * remaining bytes (if not multiple of 3). - *

- *

Note: no padding is added when encoding using the URL-safe alphabet.

- *

- * Thanks to "commons" project in ws.apache.org for the bitwise operations, and general approach. - * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ - *

- * - * @param in byte[] array of binary data to base64 encode. - * @param inPos Position to start reading data from. - * @param inAvail Amount of bytes available from input for encoding. - * @param context the context to be used - */ - private void encode(final byte[] in, int inPos, final int inAvail, final Context context) { - if (context.eof) { - return; - } - // inAvail < 0 is how we're informed of EOF in the underlying data we're - // encoding. - if (inAvail < 0) { - context.eof = true; - if (0 == context.modulus) { - return; // no leftovers to process and not using chunking - } - final byte[] buffer = ensureBufferSize(encodeSize, context); - final int savedPos = context.pos; - switch (context.modulus) { // 0-2 - case 0: // nothing to do here - break; - case 1: // 8 bits = 6 + 2 - // top 6 bits: - buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 2) & MASK_6BITS]; - // remaining 2: - buffer[context.pos++] = encodeTable[(context.ibitWorkArea << 4) & MASK_6BITS]; - break; - case 2: // 16 bits = 6 + 6 + 4 - buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 10) & MASK_6BITS]; - buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 4) & MASK_6BITS]; - buffer[context.pos++] = encodeTable[(context.ibitWorkArea << 2) & MASK_6BITS]; - break; - default: - throw new IllegalStateException("Impossible modulus " + context.modulus); - } - context.currentLinePos += context.pos - savedPos; // keep track of current line position - } else { - for (int i = 0; i < inAvail; i++) { - final byte[] buffer = ensureBufferSize(encodeSize, context); - context.modulus = (context.modulus + 1) % BYTES_PER_UNENCODED_BLOCK; - int b = in[inPos++]; - if (b < 0) { - b += 256; - } - context.ibitWorkArea = (context.ibitWorkArea << 8) + b; // BITS_PER_BYTE - if (0 == context.modulus) { // 3 bytes = 24 bits = 4 * 6 bits to extract - buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 18) & MASK_6BITS]; - buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 12) & MASK_6BITS]; - buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 6) & MASK_6BITS]; - buffer[context.pos++] = encodeTable[context.ibitWorkArea & MASK_6BITS]; - context.currentLinePos += BYTES_PER_ENCODED_BLOCK; - } - } - } - } + // If the input includes whitespace, this output array will be longer than necessary. + byte[] out = new byte[(int) (limit * 6L / 8L)]; + int outCount = 0; + int inCount = 0; - /** - * Ensure that the buffer has room for size bytes - * - * @param size minimum spare space required - * @param context the context to be used - * @return the buffer - */ - private byte[] ensureBufferSize(final int size, final Context context) { - if ((context.buffer == null) || (context.buffer.length < context.pos + size)) { - if (context.buffer == null) { - context.buffer = new byte[DEFAULT_BUFFER_SIZE]; - context.pos = 0; - context.readPos = 0; - } else { - final byte[] b = new byte[context.buffer.length * DEFAULT_BUFFER_RESIZE_FACTOR]; - System.arraycopy(context.buffer, 0, b, 0, context.buffer.length); - context.buffer = b; - } - return context.buffer; - } - return context.buffer; - } + int word = 0; + for (int pos = 0; pos < limit; pos++) { + byte c = in[pos]; - /** - *

- * Decodes all of the provided data, starting at inPos, for inAvail bytes. Should be called at least twice: once - * with the data to decode, and once with inAvail set to "-1" to alert decoder that EOF has been reached. The "-1" - * call is not necessary when decoding, but it doesn't hurt, either. - *

- *

- * Ignores all non-base64 characters. This is how chunked (e.g. 76 character) data is handled, since CR and LF are - * silently ignored, but has implications for other bytes, too. This method subscribes to the garbage-in, - * garbage-out philosophy: it will not check the provided data for validity. - *

- *

- * Thanks to "commons" project in ws.apache.org for the bitwise operations, and general approach. - * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ - *

- * - * @param in byte[] array of ascii data to base64 decode. - * @param inPos Position to start reading data from. - * @param inAvail Amount of bytes available from input for encoding. - * @param context the context to be used - */ - private void decode(final byte[] in, int inPos, final int inAvail, final Context context) { - if (context.eof) { - return; - } - if (inAvail < 0) { - context.eof = true; - } - for (int i = 0; i < inAvail; i++) { - final byte[] buffer = ensureBufferSize(decodeSize, context); - final byte b = in[inPos++]; - if (b >= 0) { - final int result = DECODE_TABLE[b]; - if (result >= 0) { - context.modulus = (context.modulus + 1) % BYTES_PER_ENCODED_BLOCK; - context.ibitWorkArea = (context.ibitWorkArea << BITS_PER_ENCODED_BYTE) + result; - if (context.modulus == 0) { - buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 16) & MASK_8BITS); - buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS); - buffer[context.pos++] = (byte) (context.ibitWorkArea & MASK_8BITS); - } - } + int bits; + if (c == '.' || c == '/' || (c >= 'A' && c <= 'z') || (c >= '0' && c <= '9')) { + bits = DECODE_TABLE[c]; + } else if (c == '\n' || c == '\r' || c == ' ' || c == '\t') { + continue; + } else { + throw new IllegalArgumentException("invalid character to decode: " + c); } - } - // Two forms of EOF as far as base64 decoder is concerned: actual - // EOF (-1) and first time '=' character is encountered in stream. - // This approach makes the '=' padding characters completely optional. - if (context.eof && context.modulus != 0) { - final byte[] buffer = ensureBufferSize(decodeSize, context); + // Append this char's 6 bits to the word. + word = (word << 6) | (byte) bits; - // We have some spare bits remaining - // Output all whole multiples of 8 bits and ignore the rest - switch (context.modulus) { - // case 0 : // impossible, as excluded above - case 1: // 6 bits - ignore entirely - // TODO not currently tested; perhaps it is impossible? - break; - case 2: // 12 bits = 8 + 4 - context.ibitWorkArea = context.ibitWorkArea >> 4; // dump the extra 4 bits - buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS); - break; - case 3: // 18 bits = 8 + 8 + 2 - context.ibitWorkArea = context.ibitWorkArea >> 2; // dump 2 bits - buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS); - buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS); - break; - default: - throw new IllegalStateException("Impossible modulus " + context.modulus); + // For every 4 chars of input, we accumulate 24 bits of output. Emit 3 bytes. + inCount++; + if (inCount % 4 == 0) { + out[outCount++] = (byte) (word >> 16); + out[outCount++] = (byte) (word >> 8); + out[outCount++] = (byte) word; } } - } - - @Override - public byte[] decode(byte[] utf8EncodedRadix64String) { - final Context c = new Context(); - decode(utf8EncodedRadix64String, 0, utf8EncodedRadix64String.length, c); - decode(utf8EncodedRadix64String, 0, -1, c); // Notify decoder of EOF. - final byte[] result = new byte[c.pos]; - readResults(result, 0, result.length, c); - return result; - } - - /** - * Holds thread context so classes can be thread-safe. - *

- * This class is not itself thread-safe; each thread must allocate its own copy. - * - * @since 1.7 - */ - static class Context { - - /** - * Place holder for the bytes we're dealing with for our based logic. - * Bitwise operations store and extract the encoding or decoding from this variable. - */ - int ibitWorkArea; - /** - * Buffer for streaming. - */ - byte[] buffer; - - /** - * Position where next character should be written in the buffer. - */ - int pos; - - /** - * Position where next character should be read from the buffer. - */ - int readPos; - - /** - * Boolean flag to indicate the EOF has been reached. Once EOF has been reached, this object becomes useless, - * and must be thrown away. - */ - boolean eof; + int lastWordChars = inCount % 4; + if (lastWordChars == 1) { + // We read 1 char followed by "===". But 6 bits is a truncated byte! Fail. + return new byte[0]; + } else if (lastWordChars == 2) { + // We read 2 chars followed by "==". Emit 1 byte with 8 of those 12 bits. + word = word << 12; + out[outCount++] = (byte) (word >> 16); + } else if (lastWordChars == 3) { + // We read 3 chars, followed by "=". Emit 2 bytes for 16 of those 18 bits. + word = word << 6; + out[outCount++] = (byte) (word >> 16); + out[outCount++] = (byte) (word >> 8); + } - /** - * Variable tracks how many characters have been written to the current line. Only used when encoding. We use - * it to make sure each encoded line never goes beyond lineLength (if lineLength > 0). - */ - int currentLinePos; + // If we sized our out array perfectly, we're done. + if (outCount == out.length) return out; - /** - * Writes to the buffer only occur after every 3/5 reads when encoding, and every 4/8 reads when decoding. This - * variable helps track that. - */ - int modulus; + // Copy the decoded bytes to a new, right-sized array. + byte[] prefix = new byte[outCount]; + System.arraycopy(out, 0, prefix, 0, outCount); + return prefix; + } - Context() { + private static byte[] encode(byte[] in, byte[] map) { + int length = 4 * (in.length / 3) + (in.length % 3 == 0 ? 0 : in.length % 3 + 1); + byte[] out = new byte[length]; + int index = 0, end = in.length - in.length % 3; + for (int i = 0; i < end; i += 3) { + out[index++] = map[(in[i] & 0xff) >> 2]; + out[index++] = map[((in[i] & 0x03) << 4) | ((in[i + 1] & 0xff) >> 4)]; + out[index++] = map[((in[i + 1] & 0x0f) << 2) | ((in[i + 2] & 0xff) >> 6)]; + out[index++] = map[(in[i + 2] & 0x3f)]; + } + switch (in.length % 3) { + case 1: + out[index++] = map[(in[end] & 0xff) >> 2]; + out[index] = map[(in[end] & 0x03) << 4]; + break; + case 2: + out[index++] = map[(in[end] & 0xff) >> 2]; + out[index++] = map[((in[end] & 0x03) << 4) | ((in[end + 1] & 0xff) >> 4)]; + out[index] = map[((in[end + 1] & 0x0f) << 2)]; + break; } + return out; } } } diff --git a/modules/bcrypt/src/test/java/at/favre/lib/crypto/bcrypt/Radix64Test.java b/modules/bcrypt/src/test/java/at/favre/lib/crypto/bcrypt/Radix64Test.java index 982f1e3..7684575 100644 --- a/modules/bcrypt/src/test/java/at/favre/lib/crypto/bcrypt/Radix64Test.java +++ b/modules/bcrypt/src/test/java/at/favre/lib/crypto/bcrypt/Radix64Test.java @@ -87,25 +87,25 @@ public void setUp() { @Repeat(3) public void testEncodeDifferentLengths() { for (int i = 1; i < 128; i++) { - testSingleEncode(i); + testSingleEncode(i, encoder); } } @Test public void testEncode16Bytes() { for (int i = 0; i < 256; i++) { - testSingleEncode(16); + testSingleEncode(16, encoder); } } @Test public void testEncode23Bytes() { for (int i = 0; i < 256; i++) { - testSingleEncode(23); + testSingleEncode(23, encoder); } } - private void testSingleEncode(int length) { + private static void testSingleEncode(int length, Radix64Encoder encoder) { byte[] rnd = Bytes.random(length).array(); byte[] encoded = encoder.encode(rnd); byte[] decoded = encoder.decode(encoded); @@ -124,7 +124,9 @@ private void testSingleEncode(int length) { public void testEncodeAgainstRefTable() { for (TestCase encodeTestCase : referenceRadix64Table) { byte[] encoded = encoder.encode(encodeTestCase.raw); - assertArrayEquals(encodeTestCase.encoded.getBytes(StandardCharsets.UTF_8), encoded); + assertArrayEquals("ref test for '" + encodeTestCase.encoded + "' did not pass - expected " + + Bytes.wrap(encodeTestCase.encoded.getBytes(StandardCharsets.UTF_8)).encodeHex() + " actual " + + Bytes.wrap(encoded).encodeHex(), encodeTestCase.encoded.getBytes(StandardCharsets.UTF_8), encoded); } } @@ -138,7 +140,7 @@ public void testDecodeAgainstRefTable() { @Test public void testBigBlob() { - testSingleEncode(1024 * 1024 * 10); + testSingleEncode(1024 * 1024 * 10, encoder); } @Test