import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
class NavicatPassword {
private int version = 0;
private final String aesKeyStr = "libcckeylibcckey";
private final String aesIvStr = "libcciv libcciv ";
private final String blowString = "3DC5CA39";
private final byte[] blowKey;
private final byte[] blowIv;
public NavicatPassword(int version) {
this.version = version;
this.blowKey = sha1(blowString).getBytes(StandardCharsets.ISO_8859_1);
this.blowIv = hexStringToByteArray("d9c7c3c8870d64bd");
}
public String encrypt(String string) throws Exception {
switch (version) {
case 11:
return encryptEleven(string);
case 12:
return encryptTwelve(string);
default:
throw new IllegalArgumentException("Unsupported version");
}
}
private String encryptEleven(String string) throws Exception {
int round = string.length() / 8;
int leftLength = string.length() % 8;
StringBuilder result = new StringBuilder();
byte[] currentVector = blowIv.clone();
for (int i = 0; i < round; i++) {
byte[] block = xorBytes(string.substring(i * 8, (i + 1) * 8).getBytes(StandardCharsets.ISO_8859_1), currentVector);
byte[] encryptedBlock = encryptBlock(block, "Blowfish/ECB/NoPadding");
currentVector = xorBytes(currentVector, encryptedBlock);
result.append(new String(encryptedBlock, StandardCharsets.ISO_8859_1));
}
if (leftLength > 0) {
int padding = 8 - leftLength;
byte[] paddedBlock = new byte[8];
System.arraycopy(string.getBytes(StandardCharsets.ISO_8859_1), round * 8, paddedBlock, 0, leftLength);
for (int i = leftLength; i < 8; i++) {
paddedBlock[i] = (byte) padding;
}
byte[] encryptedLastBlock = encryptBlock(blowIv, "Blowfish/ECB/NoPadding");
byte[] xoredLastBlock = xorBytes(paddedBlock, encryptedLastBlock);
result.append(new String(xoredLastBlock, StandardCharsets.ISO_8859_1));
}
return toHexString(result.toString().getBytes(StandardCharsets.ISO_8859_1)).toUpperCase();
}
private String encryptTwelve(String string) throws Exception {
int padding = 16 - (string.length() % 16);
byte[] paddedPlaintext = new byte[string.length() + padding];
System.arraycopy(string.getBytes(StandardCharsets.ISO_8859_1), 0, paddedPlaintext, 0, string.length());
for (int i = string.length(); i < paddedPlaintext.length; i++) {
paddedPlaintext[i] = (byte) padding;
}
byte[] ciphertext = sodiumEncrypt(paddedPlaintext, aesKeyStr.getBytes(StandardCharsets.ISO_8859_1), aesIvStr.getBytes(StandardCharsets.ISO_8859_1));
return toHexString(ciphertext).toUpperCase();
}
private byte[] sodiumEncrypt(byte[] plaintext, byte[] key, byte[] iv) throws Exception {
if (key.length != 16 || iv.length != 16) {
throw new IllegalArgumentException("Invalid key or IV length");
}
byte[] ciphertext = new byte[plaintext.length];
byte[] previous = iv.clone();
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
for (int i = 0; i < plaintext.length; i += 16) {
byte[] block = new byte[16];
System.arraycopy(plaintext, i, block, 0, Math.min(16, plaintext.length - i));
byte[] xorBlock = xorBytes(block, previous);
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
byte[] encryptedBlock = cipher.doFinal(xorBlock);
System.arraycopy(encryptedBlock, 0, ciphertext, i, encryptedBlock.length);
previous = encryptedBlock;
}
return ciphertext;
}
public String decrypt(String string) throws Exception {
switch (version) {
case 11:
return decryptEleven(string);
case 12:
return decryptTwelve(string);
default:
throw new IllegalArgumentException("Unsupported version");
}
}
private String decryptEleven(String upperString) throws Exception {
byte[] stringBytes = hexStringToByteArray(upperString.toLowerCase());
int round = stringBytes.length / 8;
byte lastBlock = stringBytes.length >= 8 ? stringBytes[stringBytes.length - 8] : 0;
StringBuilder result = new StringBuilder();
byte[] currentVector = blowIv.clone();
for (int i = 0; i < round; i++) {
byte[] encryptedBlock = new byte[8];
System.arraycopy(stringBytes, i * 8, encryptedBlock, 0, 8);
byte[] decryptedBlock = decryptBlock(encryptedBlock, "Blowfish/ECB/NoPadding");
byte[] temp = xorBytes(decryptedBlock, currentVector);
currentVector = xorBytes(currentVector, encryptedBlock);
result.append(new String(temp, StandardCharsets.ISO_8859_1));
}
int padding = lastBlock & 0xFF;
if (padding >= 1 && padding <= 8) {
result.setLength(result.length() - padding);
}
return result.toString();
}
private String decryptTwelve(String upperString) throws Exception {
byte[] stringBytes = hexStringToByteArray(upperString.toLowerCase());
byte[] plaintext = sodiumDecrypt(stringBytes, aesKeyStr.getBytes(StandardCharsets.ISO_8859_1), aesIvStr.getBytes(StandardCharsets.ISO_8859_1));
int padding = plaintext[plaintext.length - 1] & 0xFF;
if (padding >= 1 && padding <= 16) {
plaintext = java.util.Arrays.copyOf(plaintext, plaintext.length - padding);
}
return new String(plaintext, StandardCharsets.ISO_8859_1);
}
private byte[] sodiumDecrypt(byte[] ciphertext, byte[] key, byte[] iv) throws Exception {
if (key.length != 16 || iv.length != 16) {
throw new IllegalArgumentException("Invalid key or IV length");
}
byte[] plaintext = new byte[ciphertext.length];
byte[] previous = iv.clone();
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
for (int i = 0; i < ciphertext.length; i += 16) {
byte[] block = new byte[16];
System.arraycopy(ciphertext, i, block, 0, Math.min(16, ciphertext.length - i));
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
byte[] decryptedBlock = cipher.doFinal(block);
byte[] xorBlock = xorBytes(decryptedBlock, previous);
System.arraycopy(xorBlock, 0, plaintext, i, xorBlock.length);
previous = block;
}
return plaintext;
}
private byte[] encryptBlock(byte[] block, String algorithm) throws Exception {
Cipher cipher = Cipher.getInstance(algorithm);
SecretKeySpec secretKeySpec = new SecretKeySpec(blowKey, "Blowfish");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
return cipher.doFinal(block);
}
private byte[] decryptBlock(byte[] block, String algorithm) throws Exception {
Cipher cipher = Cipher.getInstance(algorithm);
SecretKeySpec secretKeySpec = new SecretKeySpec(blowKey, "Blowfish");
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
return cipher.doFinal(block);
}
private byte[] xorBytes(byte[] str1, byte[] str2) {
byte[] result = new byte[str1.length];
for (int i = 0; i < str1.length; i++) {
result[i] = (byte) (str1[i] ^ str2[i]);
}
return result;
}
private String toHexString(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xFF & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
private byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i+1), 16));
}
return data;
}
private String sha1(String input) {
try {
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-1");
byte[] messageDigest = md.digest(input.getBytes(StandardCharsets.ISO_8859_1));
StringBuilder hexString = new StringBuilder();
for (byte b : messageDigest) {
String hex = Integer.toHexString(0xFF & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws Exception {
NavicatPassword navicatPassword = new NavicatPassword(12);
String decode = navicatPassword.decrypt("47C7C7A6041AF991F58DEFB22A6384EE");
System.out.println(decode);
}
}