Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge pull request #12 from karlvr/master

Java implementation
  • Loading branch information...
commit 063dd256f7b9cc274e6da0b7c1f3b578179941f0 2 parents cb15e3a + 0489dcb
@glebd authored
View
4 java/.gitignore
@@ -0,0 +1,4 @@
+/.project
+/.classpath
+/.settings
+/target
View
BIN  java/dist/cocoafob-1.0-SNAPSHOT.jar
Binary file not shown
View
35 java/pom.xml
@@ -0,0 +1,35 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>com.xk72</groupId>
+ <artifactId>cocoafob</artifactId>
+ <version>1.0-SNAPSHOT</version>
+ <packaging>jar</packaging>
+
+ <name>cocoafob</name>
+ <url>https://github.com/glebd/cocoafob/</url>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>4.11</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>commons-codec</groupId>
+ <artifactId>commons-codec</artifactId>
+ <version>1.9</version>
+ </dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk16</artifactId>
+ <version>1.46</version>
+ </dependency>
+ </dependencies>
+</project>
View
73 java/src/main/java/com/xk72/cocoafob/LicenseData.java
@@ -0,0 +1,73 @@
+package com.xk72.cocoafob;
+
+/**
+ * Represents the data used as the string data input to the CocoaFob algorithm. Extend this class
+ * and override {@link LicenseData#toLicenseStringData()} to customise the string data to match your application.
+ * @author karlvr
+ *
+ */
+public class LicenseData {
+
+ protected String productCode;
+ protected String name;
+ protected String email;
+
+ protected LicenseData() {
+ super();
+ }
+
+ public LicenseData(String productCode, String name) {
+ super();
+ this.productCode = productCode;
+ this.name = name;
+ }
+
+ public LicenseData(String productCode, String name, String email) {
+ super();
+ this.productCode = productCode;
+ this.name = name;
+ this.email = email;
+ }
+
+ public String getProductCode() {
+ return productCode;
+ }
+
+ public void setProductCode(String productCode) {
+ this.productCode = productCode;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ /**
+ * Returns the string data input for the CocoaFob algorithm. This implementation returns a comma separated string
+ * including the {@link #productCode}, {@link #name} and {@link #email} if set.
+ * @return
+ */
+ public String toLicenseStringData() {
+ StringBuilder result = new StringBuilder();
+ result.append(productCode);
+ result.append(',');
+ result.append(name);
+ if (email != null) {
+ result.append(',');
+ result.append(email);
+ }
+ return result.toString();
+ }
+
+}
View
204 java/src/main/java/com/xk72/cocoafob/LicenseGenerator.java
@@ -0,0 +1,204 @@
+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;
+ }
+
+}
View
27 java/src/main/java/com/xk72/cocoafob/LicenseGeneratorException.java
@@ -0,0 +1,27 @@
+package com.xk72.cocoafob;
+
+/**
+ * An error occurred in the license generation or verification. This generally means that the input
+ * was malformed somehow and should be rejected.
+ * @author karlvr
+ *
+ */
+public class LicenseGeneratorException extends Exception {
+
+ public LicenseGeneratorException() {
+ super();
+ }
+
+ public LicenseGeneratorException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public LicenseGeneratorException(String message) {
+ super(message);
+ }
+
+ public LicenseGeneratorException(Throwable cause) {
+ super(cause);
+ }
+
+}
View
58 java/src/test/java/com/xk72/cocoafob/LicenseGeneratorTest.java
@@ -0,0 +1,58 @@
+package com.xk72.cocoafob;
+
+import java.io.IOException;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class LicenseGeneratorTest {
+
+ @Test
+ public void testPrivateKey() throws IOException {
+ LicenseGenerator lg = new LicenseGenerator(getClass().getResource("privkey.pem"));
+ Assert.assertTrue(lg.isCanMakeLicenses());
+ Assert.assertTrue(lg.isCanVerifyLicenses());
+ }
+
+ @Test
+ public void testPublicKey() throws IOException {
+ LicenseGenerator lg = new LicenseGenerator(getClass().getResource("pubkey.pem"));
+ Assert.assertFalse(lg.isCanMakeLicenses());
+ Assert.assertTrue(lg.isCanVerifyLicenses());
+ }
+
+ @Test
+ public void testMakeLicense() throws IOException, IllegalStateException, LicenseGeneratorException {
+ LicenseGenerator lg = new LicenseGenerator(getClass().getResource("privkey.pem"));
+ String license = lg.makeLicense(new LicenseData("Test", "Karl", "karl@example.com"));
+ Assert.assertTrue(license.length() > 0);
+ }
+
+ @Test
+ public void testVerifyLicense() throws IOException, IllegalStateException, LicenseGeneratorException {
+ LicenseGenerator lg = new LicenseGenerator(getClass().getResource("privkey.pem"));
+ LicenseData licenseData = new LicenseData("Test", "Karl", "karl@example.com");
+ String license = lg.makeLicense(licenseData);
+ boolean verified = lg.verifyLicense(licenseData, license);
+ Assert.assertTrue(verified);
+ }
+
+ @Test
+ public void testVerifyLicense2() throws IOException, IllegalStateException, LicenseGeneratorException {
+ LicenseGenerator lg = new LicenseGenerator(getClass().getResource("privkey.pem"));
+ LicenseData licenseData = new LicenseData("Test", "Karl");
+ String license = lg.makeLicense(licenseData);
+ boolean verified = lg.verifyLicense(licenseData, license);
+ Assert.assertTrue(verified);
+ }
+
+ @Test
+ public void testFailedVerifyLicense() throws IOException, IllegalStateException, LicenseGeneratorException {
+ LicenseGenerator lg = new LicenseGenerator(getClass().getResource("privkey.pem"));
+ LicenseData licenseData = new LicenseData("Test", "Karl");
+ Assert.assertTrue(lg.verifyLicense(licenseData, "GAWQE-F9AVF-8YSF3-NBDUH-C6M2J-JYAYC-X692H-H65KR-A9KAQ-R9SB7-A374H-T6AH3-87TAB-CVV6K-SKUGG-A"));
+ Assert.assertFalse(lg.verifyLicense(licenseData, "GAWQE-F9AVF-8YSF3-NBDUH-C6M2J-JYAYC-X692H-H65KR-A9KAQ-R9SB7-A374H-T6AH3-87TAB-CVV6K-SKAGG-A"));
+ Assert.assertFalse(lg.verifyLicense(licenseData, "GAWQE-F9AVF-8YSF3-NBDUH-C6M2J-JYAYC-X692H-H65KR-A9KAQ-R9SB7-A374H-T6AH3-87TAB-DVV6K-SKUGG-A"));
+ }
+
+}
View
8 java/src/test/resources/com/xk72/cocoafob/privkey.pem
@@ -0,0 +1,8 @@
+-----BEGIN DSA PRIVATE KEY-----
+MIH5AgEAAkEA8wm04e0QcQRoAVJWWnUw/4rQEKbLKjujJu6oyEv7Y2oT3itY5pbO
+bgYCHEu9FBizqq7apsWYSF3YXiRjKlg10wIVALfs9eVL10PhoV6zczFpi3C7FzWN
+AkBaPhALEKlgIltHsumHdTSBqaVoR1/bmlgw/BCC13IAsW40nkFNsK1OVwjo2ocn
+3MwW4Rdq6uLm3DlENRZ5bYrTAkEA4reDYZKAl1vx+8EIMP/+2Z7ekydHfX0sTMDg
+kxhtRm6qtcywg01X847Y9ySgNepqleD+Ka2Wbucj1pOry8MoDQIVAIXgAB9GBLh4
+keUwLHBtpClnD5E8
+-----END DSA PRIVATE KEY-----
View
8 java/src/test/resources/com/xk72/cocoafob/pubkey.pem
@@ -0,0 +1,8 @@
+-----BEGIN PUBLIC KEY-----
+MIHxMIGoBgcqhkjOOAQBMIGcAkEA8wm04e0QcQRoAVJWWnUw/4rQEKbLKjujJu6o
+yEv7Y2oT3itY5pbObgYCHEu9FBizqq7apsWYSF3YXiRjKlg10wIVALfs9eVL10Ph
+oV6zczFpi3C7FzWNAkBaPhALEKlgIltHsumHdTSBqaVoR1/bmlgw/BCC13IAsW40
+nkFNsK1OVwjo2ocn3MwW4Rdq6uLm3DlENRZ5bYrTA0QAAkEA4reDYZKAl1vx+8EI
+MP/+2Z7ekydHfX0sTMDgkxhtRm6qtcywg01X847Y9ySgNepqleD+Ka2Wbucj1pOr
+y8MoDQ==
+-----END PUBLIC KEY-----
Please sign in to comment.
Something went wrong with that request. Please try again.