Skip to content

Instantly share code, notes, and snippets.

@nicky-zs
Last active February 1, 2018 10:51
Show Gist options
  • Save nicky-zs/07c31f225d04ef64924c25c5ebe05675 to your computer and use it in GitHub Desktop.
Save nicky-zs/07c31f225d04ef64924c25c5ebe05675 to your computer and use it in GitHub Desktop.
A simple, thread-safe Base62 codec for hiding the real ID and its incresing trend. No dependencies other than JDK are required. It can encode and decode 250k+ IDs per thread per second on i5-6200@2.3GHz.
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
/**
* A simple, thread-safe Base62 codec for hiding the real ID and its incresing trend.
* <p>
* DES Encryption is used. In a same ID system, the key must be the same.
* <p>
* No dependencies other than JDK are required.
* It can encode and decode 250k+ IDs per thread per second on i5-6200@2.3GHz.
*/
public class Base62IdCodec {
private final ThreadLocal<MessageDigest> digest;
private final ThreadLocal<Cipher> cipher;
private final ThreadLocal<Key> key;
/**
* @param key the key used to encrypt and decrypt
*/
public Base62IdCodec(String key) {
this(key.getBytes(StandardCharsets.UTF_8));
}
/**
* @param key the key used to encrypt and decrypt
*/
public Base62IdCodec(byte[] key) {
digest = ThreadLocal.withInitial(() -> {
try {
return MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 MessageDigest is not supported!", e);
}
});
cipher = ThreadLocal.withInitial(() -> {
try {
return Cipher.getInstance("DES/ECB/NoPadding");
} catch (Exception e) {
throw new RuntimeException("DES/ECB/NoPadding Cipher is not supported!", e);
}
});
final MessageDigest digest = this.digest.get();
this.key = ThreadLocal.withInitial(() -> {
final DESKeySpec keySpec;
try {
keySpec = new DESKeySpec(digest.digest(key));
} catch (InvalidKeyException e) {
throw new RuntimeException("Should never happen!", e);
}
final SecretKeyFactory keyFactory;
try {
keyFactory = SecretKeyFactory.getInstance("DES");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("DES SecretKeyFactory is not supported!", e);
}
try {
return keyFactory.generateSecret(keySpec);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("Should never happen!", e);
}
});
}
public String encodeId(long idLong) {
final Cipher cipher = this.cipher.get();
final Key key = this.key.get();
try {
cipher.init(Cipher.ENCRYPT_MODE, key);
} catch (InvalidKeyException e) {
throw new RuntimeException("Should never happen!", e);
}
final byte[] resultBytes;
try {
resultBytes = cipher.doFinal(LongUtils.toByteArray(idLong));
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new RuntimeException("Should never happen!", e);
}
return Base62Utils.encode(LongUtils.fromByteArray(resultBytes));
}
public long decodeId(String idStr) {
final Cipher cipher = this.cipher.get();
final Key key = this.key.get();
try {
cipher.init(Cipher.DECRYPT_MODE, key);
} catch (InvalidKeyException e) {
throw new RuntimeException("Should never happen!", e);
}
final byte[] resultBytes;
try {
resultBytes = cipher.doFinal(LongUtils.toByteArray(Base62Utils.decode(idStr)));
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new RuntimeException("Should never happen!", e);
}
return LongUtils.fromByteArray(resultBytes);
}
private static final class Base62Utils {
private static final String R = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
private static final char[] RADIX = R.toCharArray();
private static final BigInteger RADIX_N = BigInteger.valueOf(RADIX.length);
private static final BigInteger[] REVERSE;
private static final BigInteger MAX_VALUE = new BigInteger(1, LongUtils.toByteArray(0xFFFFFFFF));
private static final String MAX_STR = encode(MAX_VALUE.longValue());
private static final int MAX_LENGTH = MAX_STR.length();
static {
REVERSE = new BigInteger[128];
for (int i = 0; i < RADIX.length; ++i) {
if (RADIX[i] >= REVERSE.length) {
throw new IllegalStateException("Invalid radix string for char: " + RADIX[i]);
}
REVERSE[RADIX[i]] = BigInteger.valueOf(i);
}
}
private static String encode(long l) {
BigInteger i = new BigInteger(1, LongUtils.toByteArray(l));
if (i.equals(BigInteger.ZERO)) {
return String.valueOf(RADIX[0]);
}
StringBuilder sb = new StringBuilder();
while (i.compareTo(BigInteger.ZERO) > 0) {
sb.append(RADIX[i.mod(RADIX_N).intValue()]);
i = i.divide(RADIX_N);
}
return sb.reverse().toString();
}
private static long decode(String str) {
if (str.length() > MAX_LENGTH) {
throw new IllegalArgumentException("Invalid input length, the max is: " + MAX_LENGTH);
}
char[] chars = str.toCharArray();
for (int i = 0; i < chars.length / 2; ++i) {
int j = chars.length - 1 - i;
char c = chars[i];
chars[i] = chars[j];
chars[j] = c;
}
BigInteger ret = BigInteger.ZERO;
BigInteger e = BigInteger.ONE;
for (int i = 0; i < chars.length; ++i) {
BigInteger r;
if (chars[i] >= REVERSE.length || (r = REVERSE[chars[i]]) == null) {
throw new IllegalArgumentException("Invalid input for char: " + chars[i]);
}
ret = ret.add(r.multiply(e));
e = e.multiply(RADIX_N);
}
if (ret.compareTo(MAX_VALUE) > 0) {
throw new IllegalArgumentException("Invalid input exceeding the max(" + MAX_STR + "): " + str);
}
return ret.longValue();
}
}
private static final class LongUtils {
private static byte[] toByteArray(long value) {
return new byte[]{(byte) ((value >> 56) & 255L), (byte) ((value >> 48) & 255L),
(byte) ((value >> 40) & 255L), (byte) ((value >> 32) & 255L), (byte) ((value >> 24) & 255L),
(byte) ((value >> 16) & 255L), (byte) ((value >> 8) & 255L), (byte) (value & 255L)};
}
private static long fromByteArray(byte[] bytes) {
if (bytes.length != 8) {
throw new IllegalArgumentException("bytes.length must be 8");
}
return ((long) bytes[0] & 255L) << 56 | ((long) bytes[1] & 255L) << 48 | ((long) bytes[2] & 255L) << 40 |
((long) bytes[3] & 255L) << 32 | ((long) bytes[4] & 255L) << 24 | ((long) bytes[5] & 255L) << 16 |
((long) bytes[6] & 255L) << 8 | (long) bytes[7] & 255L;
}
}
public static void main(String[] args) {
Base62IdCodec base62IdCodec = new Base62IdCodec("abcdefg");
final long startId = System.currentTimeMillis();
final int N = 1000000;
final long endId = startId + N;
final long startTime = System.currentTimeMillis();
for (long id = startId; id < endId; ++id) {
base62IdCodec.encodeId(id);
}
final long cost = System.currentTimeMillis() - startTime;
System.out.println(N + " id used: " + cost / 1000.0 + "s");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment