Commit 5cef57db authored by Patrik Dufresne's avatar Patrik Dufresne

Change license format to use property file with a base64 signature.

Bump version to 0.4-SNAPSHOT
parent f3bc1bcb
......@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.patrikdufresne.license</groupId>
<artifactId>com.patrikdufresne.license</artifactId>
<version>0.3-SNAPSHOT</version>
<version>0.4-SNAPSHOT</version>
<build>
<pluginManagement>
<plugins>
......
/**
* Copyright(C) 2013 Patrik Dufresne Service Logiciel <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 class is an adaptation of the BASE64Encoder provided in CA Spectrum RESTful Examples:
* /usr/Spectrum/RestfulExamples/src/test/utils/BASE64Encoder.java
*
* <p>
* This class is used to encode the username-password into base64 as request by the HTTP header definition.
*
* Modify by : Patrik Dufresne
*/
public final class Base64 {
private static final int BASELENGTH = 255;
private static final int EIGHTBIT = 8;
private static final int FOURBYTE = 4;
private static final int LOOKUPLENGTH = 64;
private static final byte PAD = (byte) '=';
private static final int SIGN = -128;
private static final int SIXTEENBIT = 16;
private static final int TWENTYFOURBITGROUP = 24;
private static byte[] base64Alphabet = new byte[BASELENGTH];
private static byte[] lookUpBase64Alphabet = new byte[LOOKUPLENGTH];
static {
for (int i = 0; i < BASELENGTH; i++) {
base64Alphabet[i] = -1;
}
for (int i = 'Z'; i >= 'A'; i--) {
base64Alphabet[i] = (byte) (i - 'A');
}
for (int i = 'z'; i >= 'a'; i--) {
base64Alphabet[i] = (byte) (i - 'a' + 26);
}
for (int i = '9'; i >= '0'; i--) {
base64Alphabet[i] = (byte) (i - '0' + 52);
}
base64Alphabet['+'] = 62;
base64Alphabet['/'] = 63;
for (int i = 0; i <= 25; i++) {
lookUpBase64Alphabet[i] = (byte) ('A' + i);
}
for (int i = 26, j = 0; i <= 51; i++, j++) {
lookUpBase64Alphabet[i] = (byte) ('a' + j);
}
for (int i = 52, j = 0; i <= 61; i++, j++) {
lookUpBase64Alphabet[i] = (byte) ('0' + j);
}
lookUpBase64Alphabet[62] = (byte) '+';
lookUpBase64Alphabet[63] = (byte) '/';
}
/**
* Decodes Base64 data into octects
*
* @param base64Data
* byte array containing Base64 data
* @return Array containing decoded data.
*/
public static byte[] decode(byte[] base64Data) {
// Should we throw away anything not in base64Data ?
// handle the edge case, so we don't have to worry about it later
if (base64Data.length == 0) {
return new byte[0];
}
int numberQuadruple = base64Data.length / FOURBYTE;
byte decodedData[] = null;
byte b1 = 0, b2 = 0, b3 = 0, b4 = 0, marker0 = 0, marker1 = 0;
int encodedIndex = 0;
int dataIndex = 0;
{
// this block sizes the output array properly - rlw
int lastData = base64Data.length;
// ignore the '=' padding
while (base64Data[lastData - 1] == PAD) {
if (--lastData == 0) {
return new byte[0];
}
}
decodedData = new byte[lastData - numberQuadruple];
}
for (int i = 0; i < numberQuadruple; i++) {
dataIndex = i * 4;
marker0 = base64Data[dataIndex + 2];
marker1 = base64Data[dataIndex + 3];
b1 = base64Alphabet[base64Data[dataIndex]];
b2 = base64Alphabet[base64Data[dataIndex + 1]];
if (marker0 != PAD && marker1 != PAD) { // No PAD e.g 3cQl
b3 = base64Alphabet[marker0];
b4 = base64Alphabet[marker1];
decodedData[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
decodedData[encodedIndex + 1] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
decodedData[encodedIndex + 2] = (byte) (b3 << 6 | b4);
} else if (marker0 == PAD) { // Two PAD e.g. 3c[Pad][Pad]
decodedData[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
} else if (marker1 == PAD) { // One PAD e.g. 3cQ[Pad]
b3 = base64Alphabet[marker0];
decodedData[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
decodedData[encodedIndex + 1] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
}
encodedIndex += 3;
}
return decodedData;
}
/**
* Encodes hex octects into Base64
*
* @param binaryData
* Array containing binaryData
* @return Base64-encoded array
*/
public static String encode(byte[] binaryData) {
int lengthDataBits = binaryData.length * EIGHTBIT;
int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP;
int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP;
byte encodedData[] = null;
if (fewerThan24bits != 0) {
// data not divisible by 24 bit
encodedData = new byte[(numberTriplets + 1) * 4];
} else {
// 16 or 8 bit
encodedData = new byte[numberTriplets * 4];
}
byte k = 0;
byte l = 0;
byte b1 = 0;
byte b2 = 0;
byte b3 = 0;
int encodedIndex = 0;
int dataIndex = 0;
int i = 0;
for (i = 0; i < numberTriplets; i++) {
dataIndex = i * 3;
b1 = binaryData[dataIndex];
b2 = binaryData[dataIndex + 1];
b3 = binaryData[dataIndex + 2];
l = (byte) (b2 & 0x0f);
k = (byte) (b1 & 0x03);
encodedIndex = i * 4;
byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0);
byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc);
encodedData[encodedIndex] = lookUpBase64Alphabet[val1];
encodedData[encodedIndex + 1] = lookUpBase64Alphabet[val2 | (k << 4)];
encodedData[encodedIndex + 2] = lookUpBase64Alphabet[(l << 2) | val3];
encodedData[encodedIndex + 3] = lookUpBase64Alphabet[b3 & 0x3f];
}
// form integral number of 6-bit groups
dataIndex = i * 3;
encodedIndex = i * 4;
if (fewerThan24bits == EIGHTBIT) {
b1 = binaryData[dataIndex];
k = (byte) (b1 & 0x03);
byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
encodedData[encodedIndex] = lookUpBase64Alphabet[val1];
encodedData[encodedIndex + 1] = lookUpBase64Alphabet[k << 4];
encodedData[encodedIndex + 2] = PAD;
encodedData[encodedIndex + 3] = PAD;
} else if (fewerThan24bits == SIXTEENBIT) {
b1 = binaryData[dataIndex];
b2 = binaryData[dataIndex + 1];
l = (byte) (b2 & 0x0f);
k = (byte) (b1 & 0x03);
byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0);
encodedData[encodedIndex] = lookUpBase64Alphabet[val1];
encodedData[encodedIndex + 1] = lookUpBase64Alphabet[val2 | (k << 4)];
encodedData[encodedIndex + 2] = lookUpBase64Alphabet[l << 2];
encodedData[encodedIndex + 3] = PAD;
}
return new String(encodedData);
}
public static boolean isArrayByteBase64(byte[] arrayOctect) {
int length = arrayOctect.length;
if (length == 0) {
return true;
}
for (int i = 0; i < length; i++) {
if (!isBase64(arrayOctect[i])) {
return false;
}
}
return true;
}
static boolean isBase64(byte octect) {
// Should we ignore white space?
return (octect == PAD || base64Alphabet[octect] != -1);
}
public static boolean isBase64(String isValidString) {
return isArrayByteBase64(isValidString.getBytes());
}
}
......@@ -33,25 +33,15 @@ 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.
* 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 SIZE = 2048;
/**
* Single instance of the utility class.
*/
// private static EncryptionManager instance;
// private static final String PUBLIC_KEY_FILE = "/License/public_key.der";
// private static final String PRIVATE_KEY_FILE =
// "/path/to/your/private_key.der";
private static final int BUF_SIZE = 4096;
private PublicKey publicKey;
......@@ -70,8 +60,7 @@ public class EncryptionManager {
* @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.
* 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) {
......@@ -100,8 +89,8 @@ public class EncryptionManager {
*/
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(SIZE);
byte[] buf = new byte[SIZE];
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);
......@@ -110,16 +99,14 @@ public class EncryptionManager {
}
/**
* This function maybe used to read the public and/or private key from a
* file.
* 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
* 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);
......@@ -144,8 +131,7 @@ public class EncryptionManager {
* @throws InvalidKeyException
* if the key is invalid.
* @throws SignatureException
* if this signature algorithm is unable to process the input
* data
* if this signature algorithm is unable to process the input data
*/
public boolean verify(byte[] data, byte[] sig) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
......@@ -162,20 +148,17 @@ public class EncryptionManager {
}
/**
* Sign the given input stream data. The signature is append to the output
* stream.
* 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.
* 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.
* if this signature algorithm is unable to process the input data provided.
* @throws UnsupportedOperationException
* if the private key was not providedin the constructor.
*/
......
......@@ -16,168 +16,132 @@
package com.patrikdufresne.license;
import java.io.Serializable;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Abstract implementation of the license interface use by the license manager.
* This class represent the license information. The {@link License} class should be used to store related information
* about the license. This information may then be saved into an encrypted file.
*
* @author Patrik Dufresne
*
*/
public abstract class AbstractLicense implements Serializable, ILicense {
public final class License {
private static final long serialVersionUID = -144058457108187374L;
public static final String EMAIL = "email";
public static final String EXPIRATION = "expiration";
public static final String ID = "id";
public static final String LICENSE_NUMBER = "licenseNumber";
public static final String LICENSE_TYPE = "licenseType";
public static final String NAME = "name";
/**
* License type for lifetime version. Always valid.
*/
public static final String TYPE_LIFETIME = "lifetime";
/**
* License type for single version. This type is valid for the given
* version.
* License type for single version. This type is valid for the given version.
*/
public static final String TYPE_SINGLE_VERSION = "single-version";
/**
* License type for trial version. This type is valid until the expiration
* date.
* License type for trial version. This type is valid until the expiration date.
*/
public static final String TYPE_TRIAL = "trial";
private String email;
private Date expiration;
private String licenseNumber;
private String licenseType;
private String name;
private String version;
/**
* Create a new license with default property value.
*/
public AbstractLicense() {
name = "";
email = "";
licenseNumber = "";
expiration = new Date();
version = "";
licenseType = TYPE_TRIAL;
}
/**
* Return the associated email value.
*
* @return the email or null
*/
public String getEmail() {
return email;
}
public static final String VERSION = "version";
/**
* @return the expiration
* Map to store properties.
*/
public Date getExpiration() {
return expiration;
}
private Map<String, String> properties;
/**
* @return the licenseNumber
*/
public String getLicenseNumber() {
return licenseNumber;
}
/**
* Return the license type.
*
* @return the licenseType
* Create a new license with default property value.
*/
public String getLicenseType() {
return licenseType;
public License() {
this.properties = new HashMap<String, String>();
// id = "";
// name = "";
// email = "";
// licenseNumber = "";
// expiration = new Date();
// version = "";
// licenseType = TYPE_TRIAL;
}
/**
* Return the name associated with the license.
* Return an unmodifiable map of properties.
*
* @return the name or null
* @return
*/
public String getName() {
return name;
public Map<String, String> getProperties() {
return Collections.unmodifiableMap(this.properties);
}
/**
* Return the license version
* Return the expiration date.
*
* @return the version or null
* @return the expiration
*/
public String getVersion() {
return version;
public Date getExpiration() {
String value = getProperty(EXPIRATION);
if (value == null || value.trim().isEmpty()) {
return null;
}
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
try {
return df.parse(value);
} catch (ParseException e) {
return null;
}
}
/**
* Sets the associated email
* Get the property value.
*
* @param email
* the email or null.
* @param key
* the property key
*/
public void setEmail(String email) {
this.email = email;
public String getProperty(String key) {
return this.properties.get(key);
}
/**
* Set the license expiration date. Required with TYPE_TRIAL
*
* @param expiration
* the expiration date or null.
* the expiration date.
*/
public void setExpiration(Date expiration) {
this.expiration = expiration;
}
/**
* Sets the license number.
*
* @param licenseNumber
* the licenseNumber
*/
public void setLicenseNumber(String licenseNumber) {
this.licenseNumber = licenseNumber;
}
/**
* Sets the license type.
*
* @param licenseType
* the licenseType, one of the TYPE_* constants (can't be null).
*/
public void setLicenseType(String licenseType) {
if (licenseType == null) {
throw new NullPointerException();
if (expiration == null) {
setProperty(EXPIRATION, null);
} else {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
setProperty(EXPIRATION, df.format(expiration));
}
this.licenseType = licenseType;
}
/**
* Sets the name associated with the license
* Sets the property value.
*
* @param name
* the name or null
*/
public void setName(String name) {
this.name = name;
}
/**
* Sets the license version. Required with SINGLE_VERSION.
*
* @param version
* the version or null
*/
public void setVersion(String version) {
this.version = version;
* @param key
* the property key
* @param value
* the property value.
*/
public void setProperty(String key, String value) {
if (value == null) {
this.properties.remove(key);
} else {
this.properties.put(key, value);
}
}
/**
......@@ -202,7 +166,7 @@ public abstract class AbstractLicense implements Serializable, ILicense {
* @throws LicenseExpiredException
*/
protected void validateExpiration(Date currentDate) throws LicenseExpiredException {
if (getLicenseType().equals(TYPE_TRIAL)) {
if (TYPE_TRIAL.equals(getProperty(LICENSE_TYPE))) {
if (getExpiration() == null || currentDate.after(getExpiration())) {
throw new LicenseExpiredException();
}
......@@ -219,11 +183,11 @@ public abstract class AbstractLicense implements Serializable, ILicense {
*/
protected void validateVersion(String currentVersion) throws LicenseVersionExpiredException {
if (getLicenseType().equals(TYPE_SINGLE_VERSION)) {
if (getVersion() == null) {
if (TYPE_SINGLE_VERSION.equals(getProperty(LICENSE_TYPE))) {
if (getProperty(VERSION) == null) {
throw new LicenseVersionExpiredException();
}
Pattern pattern = Pattern.compile(getVersion());
Pattern pattern = Pattern.compile(getProperty(VERSION));
Matcher matcher = pattern.matcher(currentVersion);
if (!matcher.matches()) {
throw new LicenseVersionExpiredException();
......
......@@ -15,14 +15,11 @@
*/
package com.patrikdufresne.license;
import java.io.Serializable;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
/**
* This interface is used to represent a license data.
*
* @author ikus060
*
*/
public interface ILicense extends Serializable {
@RunWith(Suite.class)
@Suite.SuiteClasses( { EncryptionManagerTest.class, KeyManagerTest.class, LicenseManagerTest.class })
public class AllTests {
}
......@@ -28,20 +28,13 @@ import org.junit.Before;
import org.junit.Test;
/**
* This class test all the functionnality provided by the {@link LicenseManager}
* .
* This class test all the functionnality provided by the {@link LicenseManager} .
*
* @author ikus060
*
*/
public class LicenseManagerTest {
static class MockLicense extends AbstractLicense {
private static final long serialVersionUID = -4612807836761969030L;
}
private LicenseManager manager;
@Before
......@@ -57,9 +50,18 @@ public class LicenseManagerTest {
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
*/
@Test
public void writeLicense_WithEmptyLicense_CreateTheFile() throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException {
License license = new License();
File file = new File("unittest.lic");
manager.writeLicense(license, file);
assertTrue(file.exists());
}
@Test
public void writeLicense_WithLicense_CreateTheFile() throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException {
MockLicense license = new MockLicense();
License license = new License();
license.setProperty(License.NAME, "A test name");
File file = new File("unittest.lic");
manager.writeLicense(license, file);
assertTrue(file.exists());
......@@ -81,13 +83,13 @@ public class LicenseManagerTest {
SignatureException,
IOException,
ClassNotFoundException {
MockLicense license = new MockLicense();
License license = new License();
File file = new File("unittest2.lic");
manager.writeLicense(license, file);
assertTrue(file.exists());
// Read the file
MockLicense license2 = (MockLicense) manager.readLicenseFile(file);
License license2 = (License) manager.readLicenseFile(file);
assertNotNull(license2);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment