package com.xk72.cocoafob;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;
import java.security.interfaces.DSAPrivateKey;
import java.security.interfaces.DSAPublicKey;
import org.apache.commons.codec.binary.Base32;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMReader;
/**
* Generate and verify CocoaFob licenses. Based on the PHP implementation by Sandro Noel.
* @author karlvr
*
*/
public class LicenseGenerator {
private DSAPrivateKey privateKey;
private DSAPublicKey publicKey;
private SecureRandom random;
static {
Security.addProvider(new BouncyCastleProvider());
}
protected LicenseGenerator() {
random = new SecureRandom();
}
/**
* Construct the LicenseGenerator with a URL that points to either the private key or public key.
* Pass the private key for making and verifying licenses. Pass the public key for verifying only.
* If you this code will go onto a user's machine you MUST NOT include the private key, only include
* the public key in this case.
* @param keyURL
* @throws IOException
*/
public LicenseGenerator(URL keyURL) throws IOException {
this();
initKeys(keyURL.openStream());
}
/**
* Construct the LicenseGenerator with an InputStream of either the private key or public key.
* Pass the private key for making and verifying licenses. Pass the public key for verifying only.
* If you this code will go onto a user's machine you MUST NOT include the private key, only include
* the public key in this case.
* @param keyURL
* @throws IOException
*/
public LicenseGenerator(InputStream keyInputStream) throws IOException {
this();
initKeys(keyInputStream);
}
private void initKeys(InputStream keyInputStream) throws IOException {
Object readKey = readKey(keyInputStream);
if (readKey instanceof KeyPair) {
KeyPair keyPair = (KeyPair) readKey;
privateKey = (DSAPrivateKey) keyPair.getPrivate();
publicKey = (DSAPublicKey) keyPair.getPublic();
} else if (readKey instanceof DSAPublicKey) {
publicKey = (DSAPublicKey) readKey;
} else {
throw new IllegalArgumentException("The supplied key stream didn't contain a public or private key: " + readKey.getClass());
}
}
private Object readKey(InputStream privateKeyInputSteam) throws IOException {
PEMReader pemReader = new PEMReader(new InputStreamReader(new BufferedInputStream(privateKeyInputSteam)));
try {
return pemReader.readObject();
} finally {
pemReader.close();
}
}
/**
* Make and return a license for the given {@link LicenseData}.
* @param licenseData
* @return
* @throws LicenseGeneratorException If the generation encounters an error, usually due to invalid input.
* @throws IllegalStateException If the generator is not setup correctly to make licenses.
*/
public String makeLicense(LicenseData licenseData) throws LicenseGeneratorException, IllegalStateException {
if (!isCanMakeLicenses()) {
throw new IllegalStateException("The LicenseGenerator cannot make licenses as it was not configured with a private key");
}
final String stringData = licenseData.toLicenseStringData();
try {
final Signature dsa = Signature.getInstance("SHA1withDSA", "SUN");
dsa.initSign(privateKey, random);
dsa.update(stringData.getBytes("UTF-8"));
final byte[] signed = dsa.sign();
/* base 32 encode the signature */
String result = new Base32().encodeAsString(signed);
/* replace O with 8 and I with 9 */
result = result.replace("O", "8").replace("I", "9");
/* remove padding if any. */
result = result.replace("=", "");
/* chunk with dashes */
result = split(result, 5);
return result;
} catch (NoSuchAlgorithmException e) {
throw new LicenseGeneratorException(e);
} catch (NoSuchProviderException e) {
throw new LicenseGeneratorException(e);
} catch (InvalidKeyException e) {
throw new LicenseGeneratorException(e);
} catch (SignatureException e) {
throw new LicenseGeneratorException(e);
} catch (UnsupportedEncodingException e) {
throw new LicenseGeneratorException(e);
}
}
/**
* Verify the given license for the given {@link LicenseData}.
* @param licenseData
* @param license
* @return Whether the license verified successfully.
* @throws LicenseGeneratorException If the verification encounters an error, usually due to invalid input. You MUST check the return value of this method if no exception is thrown.
* @throws IllegalStateException If the generator is not setup correctly to verify licenses.
*/
public boolean verifyLicense(LicenseData licenseData, String license) throws LicenseGeneratorException, IllegalStateException {
if (!isCanVerifyLicenses()) {
throw new IllegalStateException("The LicenseGenerator cannot verify licenses as it was not configured with a public key");
}
final String stringData = licenseData.toLicenseStringData();
/* replace O with 8 and I with 9 */
String licenseSignature = license.replace("8", "O").replace("9", "I");
/* remove dashes */
licenseSignature = licenseSignature.replace("-", "");
/* Pad the output length to a multiple of 8 with '=' characters */
while (licenseSignature.length() % 8 != 0) {
licenseSignature += "=";
}
byte[] decoded = new Base32().decode(licenseSignature);
try {
Signature dsa = Signature.getInstance("SHA1withDSA", "SUN");
dsa.initVerify(publicKey);
dsa.update(stringData.getBytes("UTF-8"));
return dsa.verify(decoded);
} catch (NoSuchAlgorithmException e) {
throw new LicenseGeneratorException(e);
} catch (NoSuchProviderException e) {
throw new LicenseGeneratorException(e);
} catch (InvalidKeyException e) {
throw new LicenseGeneratorException(e);
} catch (SignatureException e) {
throw new LicenseGeneratorException(e);
} catch (UnsupportedEncodingException e) {
throw new LicenseGeneratorException(e);
}
}
private String split(String str, int chunkSize) {
StringBuilder result = new StringBuilder();
int i = 0;
while (i < str.length()) {
if (i > 0) {
result.append('-');
}
int next = Math.min(i + chunkSize, str.length());
result.append(str.substring(i, next));
i = next;
}
return result.toString();
}
public boolean isCanMakeLicenses() {
return privateKey != null;
}
public boolean isCanVerifyLicenses() {
return publicKey != null;
}
}