Commit b17f595f authored by Patrik Dufresne's avatar Patrik Dufresne

TASK-984 Fix encoding problem

Enforce usage of UTF-8 to read and write the license file.
parent 9268c9b0
Pipeline #412 passed with stages
in 2 minutes and 15 seconds
/**
* Copyright(C) 2018 Patrik Dufresne Service Logiciel inc <info@patrikdufresne.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.patrikdufresne.license;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* This class is used to manage the encryption of the license. It's used to encrypt, sign and validate using a public or
* private key.
*
* @author Patrik Dufresne
*
*/
public class EncryptionManager {
private static final int BUF_SIZE = 4096;
private PublicKey publicKey;
/**
* Our private key.
*/
private PrivateKey privateKey;
/**
* Create a new encryption manager.
*
* @param publicKey
* the public key (can't be null).
* @param privateKey
* the private key (null if not available).
* @throws NoSuchAlgorithmException
* if no Provider supports RSA
* @throws InvalidKeySpecException
* if the given key specification is inappropriate for this key factory to produce a public key.
*/
public EncryptionManager(byte[] publicKey, byte[] privateKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
if (publicKey == null) {
throw new NullPointerException("publicKey");
}
X509EncodedKeySpec spec = new X509EncodedKeySpec(publicKey);
KeyFactory kf = KeyFactory.getInstance("RSA");
this.publicKey = kf.generatePublic(spec);
if (privateKey != null) {
PKCS8EncodedKeySpec privateSpec = new PKCS8EncodedKeySpec(privateKey);
KeyFactory privateKeyFactory = KeyFactory.getInstance("RSA");
this.privateKey = privateKeyFactory.generatePrivate(privateSpec);
}
}
/**
* This function is used to read a stream.
*
* @param input
* the input stream
* @return the data read from the stream
* @throws IOException
*/
public static byte[] readAll(InputStream input) throws IOException {
// Read the content of the file and store it in a byte array.
ByteArrayOutputStream out = new ByteArrayOutputStream(BUF_SIZE);
byte[] buf = new byte[BUF_SIZE];
int size;
while ((size = input.read(buf)) != -1) {
out.write(buf, 0, size);
}
return out.toByteArray();
}
/**
* This function maybe used to read the public and/or private key from a file.
*
* @param file
* the file to read
* @return the file data
*
* @throws IOException
* if the file does not exist, or if the first byte cannot be read for any reason
*/
public static byte[] readAll(File file) throws IOException {
InputStream input = new FileInputStream(file);
try {
return readAll(input);
} finally {
input.close();
}
}
/**
* Use to check if the given data matches the given signature.
*
* @param data
* the data
* @param sig
* the signature associated with the data.
*
* @throws NoSuchAlgorithmException
* if the algorithm SHA1withRSA is not supported.
* @throws NoSuchProviderException
* @throws InvalidKeyException
* if the key is invalid.
* @throws SignatureException
* if this signature algorithm is unable to process the input data
*/
public boolean verify(byte[] data, byte[] sig) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
// Initialize the signing algorithm with our public key
Signature rsaSignature = Signature.getInstance("SHA1withRSA");
rsaSignature.initVerify(publicKey);
// Update the signature algorithm with the data.
rsaSignature.update(data);
// Validate the signature
return rsaSignature.verify(sig);
}
/**
* Sign the given input stream data. The signature is append to the output stream.
*
* @param data
* the the data to be signed.
* @return the signature for the given data.
* @throws NoSuchAlgorithmException
* if no Provider supports a Signature implementation for SHA1withRSA.
* @throws InvalidKeyException
* if the private key is invalid.
* @throws SignatureException
* if this signature algorithm is unable to process the input data provided.
* @throws UnsupportedOperationException
* if the private key was not providedin the constructor.
*/
public byte[] sign(byte[] data) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
if (privateKey == null) {
throw new UnsupportedOperationException("Can't sign when the private key is not available.");
}
// Initialize the signing algorithm with our private key
Signature rsaSignature = Signature.getInstance("SHA1withRSA");
rsaSignature.initSign(privateKey);
rsaSignature.update(data);
// Generate the signature.
return rsaSignature.sign();
}
}
/**
* Copyright(C) 2018 Patrik Dufresne Service Logiciel inc <info@patrikdufresne.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.patrikdufresne.license;
/**
* This exception is throw by the key manager when the key is determined to be
* black listed.
*
* @author Patrik Dufresne
*
*/
public class KeyBlackListedException extends LicenseException {
private static final long serialVersionUID = 4833729281645719038L;
public KeyBlackListedException() {
super("black listed key");
}
}
/**
* Copyright(C) 2018 Patrik Dufresne Service Logiciel inc <info@patrikdufresne.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.patrikdufresne.license;
/**
* This exception is throw when the key manager determine the key as invalid
* because of the checksum or because it's been wrongly generated.
*
* @author Patrik Dufresne
*
*/
public class KeyInvalidException extends LicenseException {
private static final long serialVersionUID = 3455646784833396158L;
public KeyInvalidException() {
super("invalid key");
}
}
/**
* Copyright(C) 2018 Patrik Dufresne Service Logiciel inc <info@patrikdufresne.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.patrikdufresne.license;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashSet;
import java.util.Set;
/**
* This class is useful to generate key to identify a specific hardware using
* the network card.
*
* @author Patrik Dufresne
*
*/
public class KeyManager {
/**
* Define the default key length.
*/
private static final int DEFAULT_KEY_LENGTH = 62;
/**
* Defin the default default mac address.
*/
private static final byte[] DEFAULT_MAC_ADRESS = new byte[] { 24, 4, 124, 10, 91 };
private static final byte[][] DEFAULT_PARAMS = new byte[][] { { 24, 4, 127 }, { 10, 0, 56 }, { 1, 2, 91 }, { 7, 1, 100 } };
/**
* Calculate a checksum.
*
* @param string
* @return the check sum value
*/
private static String calculateChecksum(final String string) {
int left = 0x0056;
int right = 0x00AF;
for (byte b : string.getBytes()) {
right += b;
if (right > 0x00FF) {
right -= 0x00FF;
}
left += right;
if (left > 0x00FF) {
left -= 0x00FF;
}
}
int sum = (left << 8) + right;
return intToHex(sum, 4);
}
/**
* Get the key bytes.
*
* @param seed
* @param a
* @param b
* @param c
* @return
*/
private static byte getKeyByte(final int seed, final byte a, final byte b, final byte c) {
final int a1 = a % 25;
final int b1 = b % 3;
if (a1 % 2 == 0) {
return (byte) (((seed >> a1) & 0x000000FF) ^ ((seed >> b1) | c));
}
return (byte) (((seed >> a1) & 0x000000FF) ^ ((seed >> b1) & c));
}
/**
*
* @param n
* @param chars
* @return
*/
private static String intToHex(final Number n, final int chars) {
return String.format("%0" + chars + "x", n);
}
/**
* Sets of black listed keys.
*/
private Set<String> blacklist;
/**
* Default MAC address if no network interface is available.
*/
private byte[] defaultMac;
/**
* The current key length.
*/
private int keyLen;
/**
* Arrays used to generate and validate the key.
*/
private byte[][] params;
public KeyManager() {
this(DEFAULT_KEY_LENGTH, DEFAULT_PARAMS, DEFAULT_MAC_ADRESS);
}
/**
*
* @param keyLen
* the key length (>=8);
* @param params
* @param defaultMacAddress
* used if no network interface is available
*
*/
public KeyManager(int keyLen, byte[][] params, byte[] defaultMacAddress) {
if (keyLen < 8) {
throw new IllegalArgumentException("keyLen<8");
}
this.keyLen = keyLen;
this.params = params;
this.defaultMac = defaultMacAddress;
}
/**
* Ass the given key to the black list.
*
* @param key
* the key to be added
*/
public void addBlackListedKey(String key) {
if (this.blacklist == null) {
this.blacklist = new HashSet<String>();
}
this.blacklist.add(key);
}
/**
*
* @param seed
* @param entropy
* @return
*/
public String generateKey(final int seed, String authCode) {
byte[] entropy = getHardwareEntropy();
final byte[] keyBytes = new byte[25];
// fill keyBytes with values derived from seed.
// the parameters used here must be exactly the same
// as the ones used in the checkKey function.
keyBytes[0] = getKeyByte(seed, params[0][0], params[0][1], params[0][2]);
keyBytes[1] = getKeyByte(seed, params[1][0], params[1][1], params[1][2]);
keyBytes[2] = getKeyByte(seed, params[2][0], params[2][1], params[2][2]);
keyBytes[3] = getKeyByte(seed, params[3][0], params[3][1], params[3][2]);
for (int i = 4, j = 0; (j + 2) < entropy.length; i++, j += 3) {
keyBytes[i] = getKeyByte(seed, entropy[j], entropy[j + 1], entropy[j + 2]);
}
// The key string begins with a hexadecimal string of the seed
final StringBuilder result = new StringBuilder(intToHex(seed, 8));
// Then is followed by hexadecimal strings of each byte in the key
for (byte b : keyBytes) {
result.append(intToHex(b, 2));
}
// Add checksum to key string
String key = result.toString();
key += calculateChecksum(key);
return key;
}
/**
* Return the complete list of blackl listed key.
*
* @return
*/
public String[] getBlackListedKeys() {
if (this.blacklist == null) {
return new String[0];
}
String[] list = new String[this.blacklist.size()];
return this.blacklist.toArray(list);
}
/**
* Generate an hardate entropy based on the network address.
*
* @return the hardware entropy value.
*/
private byte[] getHardwareEntropy() {
// Get the MAC address value
byte[] mac;
try {
NetworkInterface ni = NetworkInterface.getByInetAddress(InetAddress.getLocalHost());
if (ni != null) {
mac = ni.getHardwareAddress();
if (mac == null) {
mac = defaultMac;
}
} else {
mac = defaultMac;
}
} catch (Exception ex) {
mac = defaultMac;
}
// Hash the value
byte[] entropyEncoded = null;
try {
MessageDigest digest = MessageDigest.getInstance("SHA-512");
digest.reset();
entropyEncoded = digest.digest(mac);
} catch (NoSuchAlgorithmException ex) { /* this will never happen */
}
return entropyEncoded;
}
/**
* Remove the given key from the black list.
*
* @param key
* the key to remove.
*/
public void removeBlackListedKey(String key) {
if (this.blacklist != null) {
this.blacklist.remove(key);
if (this.blacklist.size() == 0) {
this.blacklist = null;
}
}
}
/**
* Check if a key is valid.
*
* @param key
* the key to validate
* @throws KeyInvalidException
* if the key is invalid
*/
public void validateKey(final String key) throws KeyInvalidException, KeyBlackListedException {
// Validate the key checksum
if (!validateKeyChecksum(key)) {
throw new KeyInvalidException();
}
// Look at the black list.
if (this.blacklist != null) {
for (String black : this.blacklist) {
if (key.startsWith(black)) {
throw new KeyBlackListedException();
}
}
}
// At this point, the key is either valid or forged,
// because a forged key can have a valid checksum.
// we now test the "bytes" of the key to determine if it is
// actually valid.
// When building your release application, use conditional defines
// or comment out most of the byte checks! this is the heart
// of the partial key verification system. by not compiling in
// each check, there is no way for someone to build a keygen that
// will produce valid keys. if an invalid keygen is released, you can
// simply change which byte checks are compiled in, and any serial
// number built with the fake keygen no longer works.
// note that the parameters used for getKeyByte calls MUST
// MATCH the values that makeKey uses to make the key in the
// first place!
// Extract the seed from the supplied key string
final int seed;
try {
seed = Integer.valueOf(key.substring(0, 8), 16);
} catch (NumberFormatException e) {
throw new KeyInvalidException();
}
// test key 0
final String kb0 = key.substring(8, 10);
final byte b0 = getKeyByte(seed, params[0][0], params[0][1], params[0][2]);
if (!kb0.equals(intToHex(b0, 2))) {
throw new KeyInvalidException();
}
// test key1
final String kb1 = key.substring(10, 12);
final byte b1 = getKeyByte(seed, params[1][0], params[1][1], params[1][2]);
if (!kb1.equals(intToHex(b1, 2))) {
throw new KeyInvalidException();
}
// test key2
final String kb2 = key.substring(12, 14);
final byte b2 = getKeyByte(seed, params[2][0], params[2][1], params[2][2]);
if (!kb2.equals(intToHex(b2, 2))) {
throw new KeyInvalidException();
}
// test key3
final String kb3 = key.substring(14, 16);
final byte b3 = getKeyByte(seed, params[3][0], params[3][1], params[3][2]);
if (!kb3.equals(intToHex(b3, 2))) {
throw new KeyInvalidException();
}
// test the hardware entropy
byte[] encodedEntropy = getHardwareEntropy();
for (int i = 16, j = 0; (j + 2) < encodedEntropy.length; i += 2, j += 3) {
String kb = key.substring(i, i + 2);
byte b = getKeyByte(seed, encodedEntropy[j], encodedEntropy[j + 1], encodedEntropy[j + 2]);
if (!kb.equals(intToHex(b, 2))) {
throw new KeyInvalidException();
}
}
}
/**
* Validate the key check sum.
*
* @param key
* the key value
* @return
*/
private boolean validateKeyChecksum(final String key) {
if (key.length() != this.keyLen) {
throw new IllegalArgumentException("key wrong length");
}
// last four characters are the checksum
final String checksum = key.substring(this.keyLen - 4);
return checksum.equals(calculateChecksum(key.substring(0, this.keyLen - 4)));
}
}
......@@ -17,15 +17,24 @@ package com.patrikdufresne.license;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectOutputStream;
import java.io.OutputStreamWriter;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
......@@ -48,10 +57,126 @@ public class LicenseManager {
* Property to store the signature.
*/
private static final String SIGNATURE = "signature";
/**
* Define the encoding to be used to read and write the license file. Since the license may be generate on different
* platform with different default encoding, we need to hardcode this into a fixed encoding. UTF-8 should be a good
* choice.
*/
private static final String ENCODING = "UTF-8";
private static final int BUF_SIZE = 4096;
private PublicKey publicKey;
/**
* Our private key.
*/
private PrivateKey privateKey;
/**
* This function is used to read a stream.
*
* @param input
* the input stream
* @return the data read from the stream
* @throws IOException
*/
private static byte[] readAll(InputStream input) throws IOException {
if (input == null) {
return null;
}
// Read the content of the file and store it in a byte array.
ByteArrayOutputStream out = new ByteArrayOutputStream(BUF_SIZE);
byte[] buf = new byte[BUF_SIZE];
int size;
while ((size = input.read(buf)) != -1) {
out.write(buf, 0, size);
}
return out.toByteArray();
}
/**
* The encryption manager used by this class.F
* This function maybe used to read the public and/or private key from a file.
*
* @param file
* the file to read
* @return the file data
*
* @throws IOException
* if the file does not exist, or if the first byte cannot be read for any reason
*/
private EncryptionManager encryptionManager;
private static byte[] readAll(File file) throws IOException {
if (file == null) {
return null;
}
InputStream input = new FileInputStream(file);
try {
return readAll(input);
} finally {
input.close();
}
}
/**
* Use to check if the given data matches the given signature.
*
* @param data
* the data
* @param sig
* the signature associated with the data.
*
* @throws NoSuchAlgorithmException
* if the algorithm SHA1withRSA is not supported.
* @throws NoSuchProviderException