package com.plectix.license.client;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public class SecurityUtil {
public static final String LICENSE_FIELD_SEPARATOR = "-";
// This key was generated with com.plectix.license.server.KeyGenerator, and corresponds
// to the private key in com.plectix.license.server.LicenseGenerator.
private static final String PUBLIC_KEY_HEX = "305c300d06092a864886f70d0101010500034b003048024100aff3c80597c966cff656e204837c9a4dbc9e8e9c0c78330ff6445cb5c7456b73937536247890f12a189bf113c035ae70f94059bd2832b25d1c5071f04fb335d90203010001";
private static PublicKey publicKey = null;
private static final int RSA_SIGNATURE_SIZE = 64;
public static final int RSA_SIGNATURE_HEX_SIZE = 2 * RSA_SIGNATURE_SIZE;
private static final int KEY_BYTE_LENGTH = 16;
public static final String SECURITY_ALGORITHM = "RSA";
public static final String SIGNATURE_ALGORITHM = "MD5withRSA";
public static final String SECRET_KEY_SPEC_ALGORITHM = "AES";
public static final String CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
public static final String DEFAULT_CHARSET = "ISO-8859-1";
/**
* Convert a hex string to a byte array.
*
* @param hexString hex string
* @return array of bytes
*/
public static byte[] convertFromHexStringToBytes(String hexString) {
byte[] bytes = new byte[hexString.length() / 2];
for (int i = 0; i < hexString.length(); i += 2) {
int value = Integer.parseInt(hexString.substring(i, i + 2), 16);
bytes[i / 2] = (byte) value;
}
return bytes;
}
/**
*
* @param string
* @return
*/
public static final byte[] getBytes(String string) {
try {
return string.getBytes(DEFAULT_CHARSET);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
throw new RuntimeException("Charset " + DEFAULT_CHARSET + " is not supported!");
}
}
/**
*
* @param bytes
* @return
*/
private static final String getString(byte[] bytes) {
try {
return new String(bytes, DEFAULT_CHARSET);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
throw new RuntimeException("Charset " + DEFAULT_CHARSET + " is not supported!");
}
}
/**
* Read a public key from a hex string of the encoded key bytes.
*
* @param hex hex string of encoded key bytes
*
* @return public key
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
*/
public static final PublicKey readPublicKeyFromHexString(String hex)
throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] pubEncoded = SecurityUtil.convertFromHexStringToBytes(hex);
X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(pubEncoded);
KeyFactory keyFactory = KeyFactory.getInstance(SECURITY_ALGORITHM);
return keyFactory.generatePublic(pubKeySpec);
}
/**
* Compute an RSA signature (formatted as a hex string) for the specified string,
* using the supplied private key
*
* @param string String to verify
* @param signature of string, as computed by computeRSASignature()
* @param publicKey public key for private key used to sign
* @return true if signature is valid, false otherwise
* @throws SignatureException
* @throws NoSuchAlgorithmException
* @throws UnsupportedEncodingException
*/
public static final boolean validateRSASignature(String string, String hexSignature, PublicKey publicKey)
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
byte[] signatureBytes = convertFromHexStringToBytes(hexSignature);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initVerify(publicKey);
signature.update(SecurityUtil.getBytes(string));
return signature.verify(signatureBytes);
}
public static final byte[] getKeyBytes(String password) {
byte[] keyBytes = new byte[KEY_BYTE_LENGTH];
byte[] apiBytes = SecurityUtil.getBytes(password);
if (apiBytes.length >= keyBytes.length) {
System.arraycopy(apiBytes, 0, keyBytes, 0, keyBytes.length);
} else {
for (int i= 0; i< keyBytes.length; i++) {
keyBytes[i] = apiBytes[i%apiBytes.length];
}
}
return keyBytes;
}
public static final String decryptWithPassword(String encryptedText, String password) throws Exception {
byte[] keyBytes = getKeyBytes(password);
SecretKeySpec key = new SecretKeySpec(keyBytes, SECRET_KEY_SPEC_ALGORITHM);
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] cipherText = convertFromHexStringToBytes(encryptedText);
byte[] plainTextBytes = new byte[cipherText.length];
cipher.doFinal(cipherText, 0, cipherText.length, plainTextBytes, 0);
// The returned string can be longer if it is padded! So let's strip the rest:
String plainTextString = SecurityUtil.getString(plainTextBytes);
int index = plainTextString.indexOf('\0');
if (index != -1) {
plainTextString = plainTextString.substring(0, index);
}
return plainTextString;
}
/**
* Returns a license for which previously-created data has been supplied.
*
* We break the data into the signature and the data itself and make sure it's all valid.
* If it is, the other member variables of this object are initialized from the data,
* and the object can be used.
* @param apiKey
*
* @throws IllegalArgumentException
*/
public static final License getLicense(String licenseDataEncrypted, String apiKey) throws LicenseException {
boolean valid = false;
try {
String somewhatPlainText = decryptWithPassword(licenseDataEncrypted, apiKey);
// If this is first time here, initialize the key
if (publicKey == null) {
publicKey = readPublicKeyFromHexString(PUBLIC_KEY_HEX);
}
int indexOfSeparator = somewhatPlainText.indexOf(LICENSE_FIELD_SEPARATOR);
if (indexOfSeparator > 0) {
int versionNumber = Integer.parseInt(somewhatPlainText.substring(0, indexOfSeparator++));
String signatureString = somewhatPlainText.substring(indexOfSeparator, indexOfSeparator + RSA_SIGNATURE_HEX_SIZE);
String obfuscatedString = somewhatPlainText.substring(indexOfSeparator + RSA_SIGNATURE_HEX_SIZE);
String licenseDataPlain = SecurityUtil.getString(convertFromHexStringToBytes(obfuscatedString));
valid = validateRSASignature(licenseDataPlain, signatureString, publicKey);
if (valid) {
if (versionNumber == 1) {
License license = new License_V1();
license.setLicenseDataPlain(licenseDataPlain); // this method can throw an Exception
return license;
} else {
throw new LicenseException.InvalidLicenseException("Unknown version number" + versionNumber, somewhatPlainText);
}
}
}
} catch (Exception exception) {
throw new LicenseException.InvalidLicenseException("Failed to parse and validate license", licenseDataEncrypted, exception);
}
// we didn't get an Exception above... and we couldn't validate and return the license... so let's throw an Exception...
throw new LicenseException.InvalidLicenseException("Invalid signature for license data", licenseDataEncrypted);
}
}