Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Merge pull request #12 from karlvr/master

Java implementation
  • Loading branch information...
commit 063dd256f7b9cc274e6da0b7c1f3b578179941f0 2 parents cb15e3a + 0489dcb
Gleb Dolgich authored
4 java/.gitignore
... ... @@ -0,0 +1,4 @@
  1 +/.project
  2 +/.classpath
  3 +/.settings
  4 +/target
BIN  java/dist/cocoafob-1.0-SNAPSHOT.jar
Binary file not shown
35 java/pom.xml
... ... @@ -0,0 +1,35 @@
  1 +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  2 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  3 + <modelVersion>4.0.0</modelVersion>
  4 +
  5 + <groupId>com.xk72</groupId>
  6 + <artifactId>cocoafob</artifactId>
  7 + <version>1.0-SNAPSHOT</version>
  8 + <packaging>jar</packaging>
  9 +
  10 + <name>cocoafob</name>
  11 + <url>https://github.com/glebd/cocoafob/</url>
  12 +
  13 + <properties>
  14 + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  15 + </properties>
  16 +
  17 + <dependencies>
  18 + <dependency>
  19 + <groupId>junit</groupId>
  20 + <artifactId>junit</artifactId>
  21 + <version>4.11</version>
  22 + <scope>test</scope>
  23 + </dependency>
  24 + <dependency>
  25 + <groupId>commons-codec</groupId>
  26 + <artifactId>commons-codec</artifactId>
  27 + <version>1.9</version>
  28 + </dependency>
  29 + <dependency>
  30 + <groupId>org.bouncycastle</groupId>
  31 + <artifactId>bcprov-jdk16</artifactId>
  32 + <version>1.46</version>
  33 + </dependency>
  34 + </dependencies>
  35 +</project>
73 java/src/main/java/com/xk72/cocoafob/LicenseData.java
... ... @@ -0,0 +1,73 @@
  1 +package com.xk72.cocoafob;
  2 +
  3 +/**
  4 + * Represents the data used as the string data input to the CocoaFob algorithm. Extend this class
  5 + * and override {@link LicenseData#toLicenseStringData()} to customise the string data to match your application.
  6 + * @author karlvr
  7 + *
  8 + */
  9 +public class LicenseData {
  10 +
  11 + protected String productCode;
  12 + protected String name;
  13 + protected String email;
  14 +
  15 + protected LicenseData() {
  16 + super();
  17 + }
  18 +
  19 + public LicenseData(String productCode, String name) {
  20 + super();
  21 + this.productCode = productCode;
  22 + this.name = name;
  23 + }
  24 +
  25 + public LicenseData(String productCode, String name, String email) {
  26 + super();
  27 + this.productCode = productCode;
  28 + this.name = name;
  29 + this.email = email;
  30 + }
  31 +
  32 + public String getProductCode() {
  33 + return productCode;
  34 + }
  35 +
  36 + public void setProductCode(String productCode) {
  37 + this.productCode = productCode;
  38 + }
  39 +
  40 + public String getName() {
  41 + return name;
  42 + }
  43 +
  44 + public void setName(String name) {
  45 + this.name = name;
  46 + }
  47 +
  48 + public String getEmail() {
  49 + return email;
  50 + }
  51 +
  52 + public void setEmail(String email) {
  53 + this.email = email;
  54 + }
  55 +
  56 + /**
  57 + * Returns the string data input for the CocoaFob algorithm. This implementation returns a comma separated string
  58 + * including the {@link #productCode}, {@link #name} and {@link #email} if set.
  59 + * @return
  60 + */
  61 + public String toLicenseStringData() {
  62 + StringBuilder result = new StringBuilder();
  63 + result.append(productCode);
  64 + result.append(',');
  65 + result.append(name);
  66 + if (email != null) {
  67 + result.append(',');
  68 + result.append(email);
  69 + }
  70 + return result.toString();
  71 + }
  72 +
  73 +}
204 java/src/main/java/com/xk72/cocoafob/LicenseGenerator.java
... ... @@ -0,0 +1,204 @@
  1 +package com.xk72.cocoafob;
  2 +
  3 +import java.io.BufferedInputStream;
  4 +import java.io.IOException;
  5 +import java.io.InputStream;
  6 +import java.io.InputStreamReader;
  7 +import java.io.UnsupportedEncodingException;
  8 +import java.net.URL;
  9 +import java.security.InvalidKeyException;
  10 +import java.security.KeyPair;
  11 +import java.security.NoSuchAlgorithmException;
  12 +import java.security.NoSuchProviderException;
  13 +import java.security.SecureRandom;
  14 +import java.security.Security;
  15 +import java.security.Signature;
  16 +import java.security.SignatureException;
  17 +import java.security.interfaces.DSAPrivateKey;
  18 +import java.security.interfaces.DSAPublicKey;
  19 +
  20 +import org.apache.commons.codec.binary.Base32;
  21 +import org.bouncycastle.jce.provider.BouncyCastleProvider;
  22 +import org.bouncycastle.openssl.PEMReader;
  23 +
  24 +/**
  25 + * Generate and verify CocoaFob licenses. Based on the PHP implementation by Sandro Noel.
  26 + * @author karlvr
  27 + *
  28 + */
  29 +public class LicenseGenerator {
  30 +
  31 + private DSAPrivateKey privateKey;
  32 + private DSAPublicKey publicKey;
  33 + private SecureRandom random;
  34 +
  35 + static {
  36 + Security.addProvider(new BouncyCastleProvider());
  37 + }
  38 +
  39 + protected LicenseGenerator() {
  40 + random = new SecureRandom();
  41 + }
  42 +
  43 + /**
  44 + * Construct the LicenseGenerator with a URL that points to either the private key or public key.
  45 + * Pass the private key for making and verifying licenses. Pass the public key for verifying only.
  46 + * If you this code will go onto a user's machine you MUST NOT include the private key, only include
  47 + * the public key in this case.
  48 + * @param keyURL
  49 + * @throws IOException
  50 + */
  51 + public LicenseGenerator(URL keyURL) throws IOException {
  52 + this();
  53 + initKeys(keyURL.openStream());
  54 + }
  55 +
  56 + /**
  57 + * Construct the LicenseGenerator with an InputStream of either the private key or public key.
  58 + * Pass the private key for making and verifying licenses. Pass the public key for verifying only.
  59 + * If you this code will go onto a user's machine you MUST NOT include the private key, only include
  60 + * the public key in this case.
  61 + * @param keyURL
  62 + * @throws IOException
  63 + */
  64 + public LicenseGenerator(InputStream keyInputStream) throws IOException {
  65 + this();
  66 + initKeys(keyInputStream);
  67 + }
  68 +
  69 + private void initKeys(InputStream keyInputStream) throws IOException {
  70 + Object readKey = readKey(keyInputStream);
  71 + if (readKey instanceof KeyPair) {
  72 + KeyPair keyPair = (KeyPair) readKey;
  73 + privateKey = (DSAPrivateKey) keyPair.getPrivate();
  74 + publicKey = (DSAPublicKey) keyPair.getPublic();
  75 + } else if (readKey instanceof DSAPublicKey) {
  76 + publicKey = (DSAPublicKey) readKey;
  77 + } else {
  78 + throw new IllegalArgumentException("The supplied key stream didn't contain a public or private key: " + readKey.getClass());
  79 + }
  80 + }
  81 +
  82 + private Object readKey(InputStream privateKeyInputSteam) throws IOException {
  83 + PEMReader pemReader = new PEMReader(new InputStreamReader(new BufferedInputStream(privateKeyInputSteam)));
  84 + try {
  85 + return pemReader.readObject();
  86 + } finally {
  87 + pemReader.close();
  88 + }
  89 + }
  90 +
  91 + /**
  92 + * Make and return a license for the given {@link LicenseData}.
  93 + * @param licenseData
  94 + * @return
  95 + * @throws LicenseGeneratorException If the generation encounters an error, usually due to invalid input.
  96 + * @throws IllegalStateException If the generator is not setup correctly to make licenses.
  97 + */
  98 + public String makeLicense(LicenseData licenseData) throws LicenseGeneratorException, IllegalStateException {
  99 + if (!isCanMakeLicenses()) {
  100 + throw new IllegalStateException("The LicenseGenerator cannot make licenses as it was not configured with a private key");
  101 + }
  102 +
  103 + final String stringData = licenseData.toLicenseStringData();
  104 +
  105 + try {
  106 + final Signature dsa = Signature.getInstance("SHA1withDSA", "SUN");
  107 + dsa.initSign(privateKey, random);
  108 + dsa.update(stringData.getBytes("UTF-8"));
  109 +
  110 + final byte[] signed = dsa.sign();
  111 +
  112 + /* base 32 encode the signature */
  113 + String result = new Base32().encodeAsString(signed);
  114 +
  115 + /* replace O with 8 and I with 9 */
  116 + result = result.replace("O", "8").replace("I", "9");
  117 +
  118 + /* remove padding if any. */
  119 + result = result.replace("=", "");
  120 +
  121 + /* chunk with dashes */
  122 + result = split(result, 5);
  123 + return result;
  124 + } catch (NoSuchAlgorithmException e) {
  125 + throw new LicenseGeneratorException(e);
  126 + } catch (NoSuchProviderException e) {
  127 + throw new LicenseGeneratorException(e);
  128 + } catch (InvalidKeyException e) {
  129 + throw new LicenseGeneratorException(e);
  130 + } catch (SignatureException e) {
  131 + throw new LicenseGeneratorException(e);
  132 + } catch (UnsupportedEncodingException e) {
  133 + throw new LicenseGeneratorException(e);
  134 + }
  135 + }
  136 +
  137 + /**
  138 + * Verify the given license for the given {@link LicenseData}.
  139 + * @param licenseData
  140 + * @param license
  141 + * @return Whether the license verified successfully.
  142 + * @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.
  143 + * @throws IllegalStateException If the generator is not setup correctly to verify licenses.
  144 + */
  145 + public boolean verifyLicense(LicenseData licenseData, String license) throws LicenseGeneratorException, IllegalStateException {
  146 + if (!isCanVerifyLicenses()) {
  147 + throw new IllegalStateException("The LicenseGenerator cannot verify licenses as it was not configured with a public key");
  148 + }
  149 +
  150 + final String stringData = licenseData.toLicenseStringData();
  151 +
  152 + /* replace O with 8 and I with 9 */
  153 + String licenseSignature = license.replace("8", "O").replace("9", "I");
  154 +
  155 + /* remove dashes */
  156 + licenseSignature = licenseSignature.replace("-", "");
  157 +
  158 + /* Pad the output length to a multiple of 8 with '=' characters */
  159 + while (licenseSignature.length() % 8 != 0) {
  160 + licenseSignature += "=";
  161 + }
  162 +
  163 + byte[] decoded = new Base32().decode(licenseSignature);
  164 + try {
  165 + Signature dsa = Signature.getInstance("SHA1withDSA", "SUN");
  166 + dsa.initVerify(publicKey);
  167 + dsa.update(stringData.getBytes("UTF-8"));
  168 + return dsa.verify(decoded);
  169 + } catch (NoSuchAlgorithmException e) {
  170 + throw new LicenseGeneratorException(e);
  171 + } catch (NoSuchProviderException e) {
  172 + throw new LicenseGeneratorException(e);
  173 + } catch (InvalidKeyException e) {
  174 + throw new LicenseGeneratorException(e);
  175 + } catch (SignatureException e) {
  176 + throw new LicenseGeneratorException(e);
  177 + } catch (UnsupportedEncodingException e) {
  178 + throw new LicenseGeneratorException(e);
  179 + }
  180 + }
  181 +
  182 + private String split(String str, int chunkSize) {
  183 + StringBuilder result = new StringBuilder();
  184 + int i = 0;
  185 + while (i < str.length()) {
  186 + if (i > 0) {
  187 + result.append('-');
  188 + }
  189 + int next = Math.min(i + chunkSize, str.length());
  190 + result.append(str.substring(i, next));
  191 + i = next;
  192 + }
  193 + return result.toString();
  194 + }
  195 +
  196 + public boolean isCanMakeLicenses() {
  197 + return privateKey != null;
  198 + }
  199 +
  200 + public boolean isCanVerifyLicenses() {
  201 + return publicKey != null;
  202 + }
  203 +
  204 +}
27 java/src/main/java/com/xk72/cocoafob/LicenseGeneratorException.java
... ... @@ -0,0 +1,27 @@
  1 +package com.xk72.cocoafob;
  2 +
  3 +/**
  4 + * An error occurred in the license generation or verification. This generally means that the input
  5 + * was malformed somehow and should be rejected.
  6 + * @author karlvr
  7 + *
  8 + */
  9 +public class LicenseGeneratorException extends Exception {
  10 +
  11 + public LicenseGeneratorException() {
  12 + super();
  13 + }
  14 +
  15 + public LicenseGeneratorException(String message, Throwable cause) {
  16 + super(message, cause);
  17 + }
  18 +
  19 + public LicenseGeneratorException(String message) {
  20 + super(message);
  21 + }
  22 +
  23 + public LicenseGeneratorException(Throwable cause) {
  24 + super(cause);
  25 + }
  26 +
  27 +}
58 java/src/test/java/com/xk72/cocoafob/LicenseGeneratorTest.java
... ... @@ -0,0 +1,58 @@
  1 +package com.xk72.cocoafob;
  2 +
  3 +import java.io.IOException;
  4 +
  5 +import org.junit.Assert;
  6 +import org.junit.Test;
  7 +
  8 +public class LicenseGeneratorTest {
  9 +
  10 + @Test
  11 + public void testPrivateKey() throws IOException {
  12 + LicenseGenerator lg = new LicenseGenerator(getClass().getResource("privkey.pem"));
  13 + Assert.assertTrue(lg.isCanMakeLicenses());
  14 + Assert.assertTrue(lg.isCanVerifyLicenses());
  15 + }
  16 +
  17 + @Test
  18 + public void testPublicKey() throws IOException {
  19 + LicenseGenerator lg = new LicenseGenerator(getClass().getResource("pubkey.pem"));
  20 + Assert.assertFalse(lg.isCanMakeLicenses());
  21 + Assert.assertTrue(lg.isCanVerifyLicenses());
  22 + }
  23 +
  24 + @Test
  25 + public void testMakeLicense() throws IOException, IllegalStateException, LicenseGeneratorException {
  26 + LicenseGenerator lg = new LicenseGenerator(getClass().getResource("privkey.pem"));
  27 + String license = lg.makeLicense(new LicenseData("Test", "Karl", "karl@example.com"));
  28 + Assert.assertTrue(license.length() > 0);
  29 + }
  30 +
  31 + @Test
  32 + public void testVerifyLicense() throws IOException, IllegalStateException, LicenseGeneratorException {
  33 + LicenseGenerator lg = new LicenseGenerator(getClass().getResource("privkey.pem"));
  34 + LicenseData licenseData = new LicenseData("Test", "Karl", "karl@example.com");
  35 + String license = lg.makeLicense(licenseData);
  36 + boolean verified = lg.verifyLicense(licenseData, license);
  37 + Assert.assertTrue(verified);
  38 + }
  39 +
  40 + @Test
  41 + public void testVerifyLicense2() throws IOException, IllegalStateException, LicenseGeneratorException {
  42 + LicenseGenerator lg = new LicenseGenerator(getClass().getResource("privkey.pem"));
  43 + LicenseData licenseData = new LicenseData("Test", "Karl");
  44 + String license = lg.makeLicense(licenseData);
  45 + boolean verified = lg.verifyLicense(licenseData, license);
  46 + Assert.assertTrue(verified);
  47 + }
  48 +
  49 + @Test
  50 + public void testFailedVerifyLicense() throws IOException, IllegalStateException, LicenseGeneratorException {
  51 + LicenseGenerator lg = new LicenseGenerator(getClass().getResource("privkey.pem"));
  52 + LicenseData licenseData = new LicenseData("Test", "Karl");
  53 + Assert.assertTrue(lg.verifyLicense(licenseData, "GAWQE-F9AVF-8YSF3-NBDUH-C6M2J-JYAYC-X692H-H65KR-A9KAQ-R9SB7-A374H-T6AH3-87TAB-CVV6K-SKUGG-A"));
  54 + Assert.assertFalse(lg.verifyLicense(licenseData, "GAWQE-F9AVF-8YSF3-NBDUH-C6M2J-JYAYC-X692H-H65KR-A9KAQ-R9SB7-A374H-T6AH3-87TAB-CVV6K-SKAGG-A"));
  55 + Assert.assertFalse(lg.verifyLicense(licenseData, "GAWQE-F9AVF-8YSF3-NBDUH-C6M2J-JYAYC-X692H-H65KR-A9KAQ-R9SB7-A374H-T6AH3-87TAB-DVV6K-SKUGG-A"));
  56 + }
  57 +
  58 +}
8 java/src/test/resources/com/xk72/cocoafob/privkey.pem
... ... @@ -0,0 +1,8 @@
  1 +-----BEGIN DSA PRIVATE KEY-----
  2 +MIH5AgEAAkEA8wm04e0QcQRoAVJWWnUw/4rQEKbLKjujJu6oyEv7Y2oT3itY5pbO
  3 +bgYCHEu9FBizqq7apsWYSF3YXiRjKlg10wIVALfs9eVL10PhoV6zczFpi3C7FzWN
  4 +AkBaPhALEKlgIltHsumHdTSBqaVoR1/bmlgw/BCC13IAsW40nkFNsK1OVwjo2ocn
  5 +3MwW4Rdq6uLm3DlENRZ5bYrTAkEA4reDYZKAl1vx+8EIMP/+2Z7ekydHfX0sTMDg
  6 +kxhtRm6qtcywg01X847Y9ySgNepqleD+Ka2Wbucj1pOry8MoDQIVAIXgAB9GBLh4
  7 +keUwLHBtpClnD5E8
  8 +-----END DSA PRIVATE KEY-----
8 java/src/test/resources/com/xk72/cocoafob/pubkey.pem
... ... @@ -0,0 +1,8 @@
  1 +-----BEGIN PUBLIC KEY-----
  2 +MIHxMIGoBgcqhkjOOAQBMIGcAkEA8wm04e0QcQRoAVJWWnUw/4rQEKbLKjujJu6o
  3 +yEv7Y2oT3itY5pbObgYCHEu9FBizqq7apsWYSF3YXiRjKlg10wIVALfs9eVL10Ph
  4 +oV6zczFpi3C7FzWNAkBaPhALEKlgIltHsumHdTSBqaVoR1/bmlgw/BCC13IAsW40
  5 +nkFNsK1OVwjo2ocn3MwW4Rdq6uLm3DlENRZ5bYrTA0QAAkEA4reDYZKAl1vx+8EI
  6 +MP/+2Z7ekydHfX0sTMDgkxhtRm6qtcywg01X847Y9ySgNepqleD+Ka2Wbucj1pOr
  7 +y8MoDQ==
  8 +-----END PUBLIC KEY-----

0 comments on commit 063dd25

Please sign in to comment.
Something went wrong with that request. Please try again.