The following code compresses a 60-byte BCrypt MCF into 40 bytes.
Does anyone see room for performance or other optimizations?
For example, I should replace Guava's ArrayBasedUnicodeEscaper
with a more efficient algorithm that deals only with char
s.
import com.google.common.escape.ArrayBasedUnicodeEscaper;
import com.google.common.escape.UnicodeEscaper;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public final class BcryptCompressor {
private static final String BCRYPT_CHARACTERS = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
private static final String BASE64_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
private static final UnicodeEscaper BCRYPT_TO_BASE64 = new BcryptToBase64();
private static final UnicodeEscaper BASE64_TO_BCRYPT = new Base64ToBcrypt();
private static final Map<String, Byte> SCHEME_TO_BYTE_MAP = Map.ofEntries(
Map.entry("2", (byte)0x20),
Map.entry("2a", (byte)0x40),
Map.entry("2x", (byte)0x60),
Map.entry("2y", (byte)0x80),
Map.entry("2b", (byte)0xA0)
);
private static final Map<Byte, String> BYTE_TO_SCHEME_MAP = Map.ofEntries(
Map.entry((byte)0x20, "2"),
Map.entry((byte)0x40, "2a"),
Map.entry((byte)0x60, "2x"),
Map.entry((byte)0x80, "2y"),
Map.entry((byte)0xA0, "2b")
);
public static String encode(final byte[] bmcf) {
final byte header = bmcf[0];
return ('$' + BYTE_TO_SCHEME_MAP.get((byte)(header & 0xE0)) + '$' + (header & 0x1F) + '$' + bcrypt64Encode(bmcf, 1, 16) + bcrypt64Encode(bmcf, 17, 23));
}
public static byte[] decode(final String mcf) {
final int secondDollarIndex = mcf.indexOf('$', 1);
final int thirdDollarIndex = mcf.indexOf('$', (secondDollarIndex + 1));
final String schema = mcf.substring(1, secondDollarIndex);
final String cost = mcf.substring((secondDollarIndex + 1), thirdDollarIndex);
final String saltAndHash = mcf.substring((thirdDollarIndex + 1));
final ByteBuffer buffer = ByteBuffer.allocate(40);
final byte header = (byte)(SCHEME_TO_BYTE_MAP.get(schema) | (Integer.parseInt(cost) & 0x1F));
buffer.put(header);
final String salt = saltAndHash.substring(0, 22);
final String hash = saltAndHash.substring(22);
buffer.put(bcrypt64Decode(salt));
buffer.put(bcrypt64Decode(hash));
return buffer.array();
}
private static String bcrypt64Encode(final byte[] data, final int offset, final int length) {
return BASE64_TO_BCRYPT.escape(new String(Base64.getEncoder()
.withoutPadding()
.encode(Arrays.copyOfRange(data, offset, offset + length))));
}
private static byte[] bcrypt64Decode(final String data) {
return Base64.getDecoder()
.decode(BCRYPT_TO_BASE64.escape(data));
}
private static Map<Character, String> createMap(final String from, final String to) {
final Map<Character, String> result = new HashMap<>();
for(int i = 0; i < from.length(); i++) {
result.put(from.charAt(i), to.substring(i, (i + 1)));
}
return result;
}
private static final class BcryptToBase64 extends ArrayBasedUnicodeEscaper {
public BcryptToBase64() {
super(createMap(BCRYPT_CHARACTERS, BASE64_CHARACTERS), 0, 0xFFFF, null);
}
@Override
protected char[] escapeUnsafe(final int cp) {
throw new UnsupportedOperationException();
}
}
private static final class Base64ToBcrypt extends ArrayBasedUnicodeEscaper {
public Base64ToBcrypt() {
super(createMap(BASE64_CHARACTERS, BCRYPT_CHARACTERS), 0, 0xFFFF, null);
}
@Override
protected char[] escapeUnsafe(final int cp) {
throw new UnsupportedOperationException();
}
}
}