package com.atlassian.aquatic;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.spec.RSAPrivateKeySpec;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Implementation of the AquaticPrime license generator. This is based on the Python/PHP versions,
* however it uses the Java-provided RSA implementation rather then their roll-your-own.
*
* See AquaticPrime.py and AquaticPrimeLicenseGeneratorTest in the test module.
* Copyright Atlassian: 17/11/11
*/
public class AquaticPrimeLicenseGeneratorImpl implements AquaticPrimeLicenseGenerator
{
private final BigInteger privateKey;
private final BigInteger publicKey;
public AquaticPrimeLicenseGeneratorImpl(String privateKey, String publicKey)
{
this.privateKey = new BigInteger(privateKey, 16);
this.publicKey = new BigInteger(publicKey, 16);
}
public AquaticPrimeLicense generateLicense(Map<String, String> properties)
throws LicenseGenerationException
{
// It's not clear whether these implementations are thread-safe so generate here...
MessageDigest sha1Digest;
Cipher rsaCipher;
try {
sha1Digest = MessageDigest.getInstance("SHA1");
RSAPrivateKeySpec privkey = new RSAPrivateKeySpec(publicKey, privateKey);
KeyFactory factory = KeyFactory.getInstance("RSA");
rsaCipher = Cipher.getInstance("RSA");
rsaCipher.init(Cipher.ENCRYPT_MODE, factory.generatePrivate(privkey));
} catch (Exception e) { // javax.security.* throws a lot of exceptions
throw new LicenseGenerationException(e);
}
/*
* The AquaticPrime algorithm is as follows:
*
* - Concatenate the properties values in key order
* - Convert to UTF-8 byte stream
* - Hash with unsalted SHA1
* - Encrypt with RSA
*
* NOTE: Although the Python and PHP versions profess to escape
* apostrophes as '\'', in practice this is wrong. The XML document
* includes the unescaped string, and thus the decrypted hash won't
* match on verification. In practice the PHP version doesn't use the
* escaped string *when the compiled signer is missing*, which is
* presumably always the case.
*/
List<String> keys = new ArrayList<String>(properties.keySet());
Collections.sort(keys);
// Concatenate the properties together in key-order
StringBuilder str = new StringBuilder();
for (String key : keys) {
str.append(properties.get(key));
}
try {
// Encode as UTF-8
byte[] data = str.toString()
// .replace("'", "'\\''") // Not actually needed, see above
.getBytes("UTF-8");
// Hash, encrypt and base64 encode
data = sha1Digest.digest(data);
data = rsaCipher.doFinal(data);
data = Base64.encodeBase64Chunked(data);
return new AquaticPrimeLicense(properties, data);
} catch (Exception e) {
throw new LicenseGenerationException(e);
}
}
}