package org.araqne.log.api;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class StringAnon {
public static class Mask {
public long mask;
public long pad;
public Mask(long mask, long pad) {
this.mask = mask;
this.pad = pad;
}
}
private Cipher cipher;
private Mask[] masks;
private String aesKey;
private byte[] pad;
public StringAnon(String key32Byte) {
if (key32Byte.length() != 32) {
throw new IllegalArgumentException("key must me a 32 byte long string");
}
this.aesKey = key32Byte.substring(0, 16);
SecretKeySpec keyspec = new SecretKeySpec(aesKey.getBytes(), "AES");
try {
cipher = javax.crypto.Cipher.getInstance("AES/CBC/NoPadding");
cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, keyspec, new IvParameterSpec(new byte[16]));
this.pad = cipher.doFinal(key32Byte.substring(16).getBytes());
byte[] f4 = Arrays.copyOf(this.pad, 4);
long f4bp = toInt(f4);
this.masks = new Mask[32];
for (int p = 0; p < 32; ++p) {
long mask = 0xFFFFFFFFL >> (32 - p) << (32 - p);
masks[p] = new Mask(mask, f4bp & (~mask));
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
}
private long toInt(byte[] arr) {
long l0 = ((long) arr[0] & 0xffL) << (8 * (3 - 0));
long l1 = ((long) arr[1] & 0xffL) << (8 * (3 - 1));
long l2 = ((long) arr[2] & 0xffL) << (8 * (3 - 2));
long l3 = ((long) arr[3] & 0xffL) << (8 * (3 - 3));
return l0 | l1 | l2 | l3;
}
public void reset() throws IllegalBlockSizeException, BadPaddingException {
cipher.doFinal();
}
public byte[] anonymize2(byte[] b4) throws IllegalBlockSizeException, BadPaddingException {
do {
pad[0] = (byte) (b4[0] & 0xFF);
if (b4.length == 1)
break;
pad[1] = (byte) (b4[1] & 0xFF);
if (b4.length == 2)
break;
pad[2] = (byte) (b4[2] & 0xFF);
if (b4.length == 3)
break;
pad[3] = (byte) (b4[3] & 0xFF);
} while (false);
byte[] doFinal = cipher.update(pad);
return Arrays.copyOfRange(doFinal, 0, 4);
}
public byte[] anonymize(byte[] b4) {
long result = 0;
byte[] addria = b4;
long addri = toInt(addria);
long[] addresses = new long[32];
for (int i = 0; i < this.masks.length; ++i) {
addresses[i] = (addri & masks[i].mask) | masks[i].pad;
}
int[] calcResult = new int[32];
try {
for (int i = 0; i < 32; ++i) {
calcResult[i] = calc(addresses[i]);
}
result = 0;
for (int i = 0; i < 32; ++i) {
result = (result << 1) | calcResult[i];
}
byte[] rarr = toArray(result ^ addri);
return rarr;
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
throw new IllegalStateException();
}
private byte[] toArray(long n) {
byte[] result = new byte[4];
for (int i = 3; i > -1; --i) {
result[3 - i] = (byte) (n >> (i * 8) & 0xFF);
}
return result;
}
// calculate the first bit for Crypto-PAN
private int calc(long a) throws IllegalBlockSizeException, BadPaddingException {
pad[3] = (byte) (a & 0xFF);
a >>= 8;
pad[2] = (byte) (a & 0xFF);
a >>= 8;
pad[1] = (byte) (a & 0xFF);
a >>= 8;
pad[0] = (byte) (a & 0xFF);
byte[] doFinal = cipher.doFinal(pad);
return doFinal[0] < 0 ? 1 : 0;
}
public static void main(String[] args) throws InterruptedException {
StringBuilder sb = new StringBuilder();
int k = 5;
for (int i = k; i < k + 32; ++i) {
sb.append((char) i);
}
StringAnon c = new StringAnon(sb.toString());
System.out.println(c.anonymize2("stania", "utf-8", Options.PreserveNumber));
System.out.println(c.anonymize2("stania", "utf-8", Options.PreserveNumber));
System.out.println(c.anonymize2("stalia", "utf-8", Options.PreserveNumber));
System.out.println(c.anonymize2("stalia", "utf-8", Options.PreserveNumber));
System.out.println(c.anonymize2("everclear", "utf-8", Options.PreserveNumber));
System.out.println(c.anonymize2("evercIear", "utf-8", Options.PreserveNumber));
System.out.println(c.anonymize2("darkluster", "utf-8", Options.PreserveNumber));
System.out.println(c.anonymize2("blue1273", "utf-8", Options.PreserveNumber));
System.out.println(c.anonymize2("jungi2", "utf-8", Options.PreserveNumber));
System.out.println(c.anonymize("ibicegbukbo", "utf-8", Options.ConsonantOnly));
System.out.println(c.anonymize("sbiukgbcgbs", "utf-8", Options.ConsonantOnly));
long started = System.currentTimeMillis();
for (int i = 0; i < 50000; ++i) {
c.anonymize("sumbimusbmbcuc");
}
System.out.println("elapsed: " + (System.currentTimeMillis() - started));
}
private String anonymize(String src) {
return anonymize(src, "utf-8", Options.PreserveNumber);
}
byte[] ba = new byte[4];
public static enum Options {
Random,
ConsonantOnly,
PreserveNumber,
PreserveNumberAndSpace,
HexOnly,
}
public void anonymize(byte[] src, Options opt) {
try {
byte[] bytes = src;
ba[0] = ba[1] = ba[2] = ba[3] = 0;
int[] ccnt = new int[] { 0 };
for (int c = 0; c < bytes.length; c += 4) {
int r = bytes.length - c;
do {
ba[0] = bytes[c + 0];
if (r == 1)
break;
ba[1] = bytes[c + 1];
if (r == 2)
break;
ba[2] = bytes[c + 2];
if (r == 3)
break;
ba[3] = bytes[c + 3];
} while (false);
byte[] anon = anonymize(ba);
updateResult(opt, bytes, ccnt, c, r, anon);
}
reset();
} catch (IllegalBlockSizeException e) {
throw new IllegalStateException(e);
} catch (BadPaddingException e) {
throw new IllegalStateException(e);
}
}
public String anonymize(String src, String encoding, Options opt) {
try {
byte[] bytes = src.getBytes(encoding);
anonymize(bytes, opt);
return Charset.forName(encoding).decode(ByteBuffer.wrap(bytes)).toString();
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}
private void updateResult(Options opt, byte[] bytes, int[] ccnt, int c, int r, byte[] result) {
if (opt == Options.ConsonantOnly) {
do {
bytes[c + 0] = selConsonant(result[0]);
if (r == 1)
break;
bytes[c + 1] = selConsonant(result[1]);
if (r == 2)
break;
bytes[c + 2] = selConsonant(result[2]);
if (r == 3)
break;
bytes[c + 3] = selConsonant(result[3]);
} while (false);
} else if (opt == Options.PreserveNumber) {
do {
bytes[c + 0] = selPreservingNumber(bytes[c + 0], result[0], ccnt);
if (r == 1)
break;
bytes[c + 1] = selPreservingNumber(bytes[c + 1], result[1], ccnt);
if (r == 2)
break;
bytes[c + 2] = selPreservingNumber(bytes[c + 2], result[2], ccnt);
if (r == 3)
break;
bytes[c + 3] = selPreservingNumber(bytes[c + 3], result[3], ccnt);
} while (false);
} else if (opt == Options.PreserveNumberAndSpace) {
do {
bytes[c + 0] = selPreservingNumberAndSpace(bytes[c + 0], result[0], ccnt);
if (r == 1)
break;
bytes[c + 1] = selPreservingNumberAndSpace(bytes[c + 1], result[1], ccnt);
if (r == 2)
break;
bytes[c + 2] = selPreservingNumberAndSpace(bytes[c + 2], result[2], ccnt);
if (r == 3)
break;
bytes[c + 3] = selPreservingNumberAndSpace(bytes[c + 3], result[3], ccnt);
} while (false);
} else if (opt == Options.HexOnly) {
do {
bytes[c + 0] = selHexChar(result[0]);
if (r == 1)
break;
bytes[c + 1] = selHexChar(result[1]);
if (r == 2)
break;
bytes[c + 2] = selHexChar(result[2]);
if (r == 3)
break;
bytes[c + 3] = selHexChar(result[3]);
} while (false);
} else {
do {
bytes[c + 0] = selRandomChar(result[0]);
if (r == 1)
break;
bytes[c + 1] = selRandomChar(result[1]);
if (r == 2)
break;
bytes[c + 2] = selRandomChar(result[2]);
if (r == 3)
break;
bytes[c + 3] = selRandomChar(result[3]);
} while (false);
}
}
public String anonymize2(String src, String encoding, Options opt) {
try {
byte[] bytes = src.getBytes(encoding);
ba[0] = ba[1] = ba[2] = ba[3] = 0;
int[] ccnt = new int[] { 0 };
for (int c = 0; c < bytes.length; c += 4) {
int r = bytes.length - c;
byte[] result = anonymize2(Arrays.copyOfRange(bytes, c, c + 4));
updateResult(opt, bytes, ccnt, c, r, result);
}
reset();
return Charset.forName(encoding).decode(ByteBuffer.wrap(bytes)).toString();
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
} catch (IllegalBlockSizeException e) {
throw new IllegalStateException(e);
} catch (BadPaddingException e) {
throw new IllegalStateException(e);
}
}
private byte selPreservingNumber(byte s, byte b, int[] ccnt) {
if (isDigit(s)) {
ccnt[0] = 0;
return dictn[(b & 0xff) % dictn.length];
} else {
return selReadableChar(b, ccnt);
}
}
private byte selPreservingNumberAndSpace(byte s, byte b, int[] ccnt) {
if (isDigit(s)) {
ccnt[0] = 0;
return dictn[(b & 0xff) % dictn.length];
} else if (s == ' ') {
return ' ';
} else {
return selReadableChar(b, ccnt);
}
}
private byte selReadableChar(byte b, int[] ccnt) {
if (ccnt[0]++ % 2 == 0)
return dictc[(b & 0xff) % dictc.length];
else
return dictv[(b & 0xff) % dictv.length];
}
private byte selConsonant(byte b) {
return dictc[(b & 0xff) % dictc.length];
}
private byte selRandomChar(byte b) {
return dicta[(b & 0xff) % dicta.length];
}
private byte selHexChar(byte b) {
return dicth[(b & 0xff) % dicth.length];
}
private boolean isDigit(byte b) {
return b >= '0' && b <= '9';
}
static byte[] dicta = 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', '2', '3', '4', '5', '6', '7' };
static byte[] dicth = new byte[] {
'a', 'b', 'c', 'd', 'e', 'f', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
static byte[] dictc = new byte[] {
'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'n', 'p', 's', 'r', 't', 'v', 'w', 'x', 'y', 'z' };
static byte[] dictv = new byte[] {
'a', 'i', 'u', 'o', 'e', 'y', 's' };
static byte[] dictn = new byte[] {
'2', '1', '9', '7', '5', '3', '0', '6', '4', '8', '3', '0', '6', '1', '5', '2', '4', '8', '7', '9' };
}