Monday Oct 19, 2009

Updated Java Store LRM Sample

As Octavian mentioned in his recent blog entry, Java Store and License Management, the Java Store will soon offer a simple license rights management system to developers. Octavain's post was based upon early work and, because there have been questions about it, I'm posting updated sample source code for the license management feature. Enjoy!

Java Store License Rights Management Sample
package com.sun.javastore.licenserightsapp;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.security.Signature;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Date;
import java.util.Map;
import java.util.Properties;

/\*\*
 \* A sample application demonstrating how to decode and verify the Java Store licensing rights object. This
 \* sample shows the rights code executing within the {@code main()} method that normally starts your
 \* application. In a real application you should move this code to wherever your application decides which
 \* functions are available.  Note: An application can be run in demo/preview mode, in which case, none of
 \* the rights objects are to be set, which is an indicator to the application that it is being run in such
 \* mode.
 \*
 \* You may wish to use several copies of the code in different places in your
 \* application to check for different rights. You should try to avoid using a pattern like :
 \*
 \*  
 \*  public boolean featureUnlocked() {
 \*     ... rights handling code ...
 \*  }
 \* 
\* \* as this is quite simple to replace with a method that simply returns "true" all the time. The more \* you spread out the rights processing and checking within your code the harder it will be for attackers \* to disable it. \*/ public class App { /\*\* \* The public key which must match the key which was used for signing the rights object. \*/ private final static byte[] TRUSTED_PUBLIC_KEY = { (byte) 0x30, (byte) 0x81, (byte) 0x9F, (byte) 0x30, (byte) 0x0D, (byte) 0x06, (byte) 0x09, (byte) 0x2A, (byte) 0x86, (byte) 0x48, (byte) 0x86, (byte) 0xF7, (byte) 0x0D, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x05, (byte) 0x00, (byte) 0x03, (byte) 0x81, (byte) 0x8D, (byte) 0x00, (byte) 0x30, (byte) 0x81, (byte) 0x89, (byte) 0x02, (byte) 0x81, (byte) 0x81, (byte) 0x00, (byte) 0x98, (byte) 0x21, (byte) 0xAE, (byte) 0x6C, (byte) 0x80, (byte) 0x97, (byte) 0x6F, (byte) 0xDE, (byte) 0xAD, (byte) 0xC4, (byte) 0x59, (byte) 0x44, (byte) 0x57, (byte) 0xCC, (byte) 0x3D, (byte) 0x7E, (byte) 0x48, (byte) 0x7B, (byte) 0xE2, (byte) 0xEE, (byte) 0xB2, (byte) 0xA3, (byte) 0xF7, (byte) 0x53, (byte) 0x03, (byte) 0x12, (byte) 0x07, (byte) 0x60, (byte) 0x3D, (byte) 0x5E, (byte) 0x5D, (byte) 0xEF, (byte) 0x89, (byte) 0xAF, (byte) 0xA4, (byte) 0x6E, (byte) 0x85, (byte) 0xE3, (byte) 0xB8, (byte) 0xD1, (byte) 0xF1, (byte) 0x4D, (byte) 0x8C, (byte) 0x8C, (byte) 0x10, (byte) 0xE5, (byte) 0x1A, (byte) 0x6F, (byte) 0x70, (byte) 0xE8, (byte) 0xAD, (byte) 0x54, (byte) 0x96, (byte) 0xB1, (byte) 0x61, (byte) 0xC5, (byte) 0xE5, (byte) 0x42, (byte) 0x83, (byte) 0x2B, (byte) 0xB2, (byte) 0xB1, (byte) 0x91, (byte) 0x6F, (byte) 0xB0, (byte) 0xFE, (byte) 0x0D, (byte) 0xAF, (byte) 0x60, (byte) 0xA9, (byte) 0xD5, (byte) 0x64, (byte) 0x82, (byte) 0x8F, (byte) 0x1F, (byte) 0xE5, (byte) 0x47, (byte) 0x45, (byte) 0x76, (byte) 0x00, (byte) 0xC1, (byte) 0x43, (byte) 0x29, (byte) 0x10, (byte) 0x33, (byte) 0x52, (byte) 0x49, (byte) 0x53, (byte) 0x0E, (byte) 0xC8, (byte) 0xD2, (byte) 0x98, (byte) 0x17, (byte) 0x7F, (byte) 0x6E, (byte) 0x8F, (byte) 0x41, (byte) 0x1C, (byte) 0xF6, (byte) 0xE4, (byte) 0x6F, (byte) 0x84, (byte) 0x1C, (byte) 0x39, (byte) 0xA7, (byte) 0x17, (byte) 0x8A, (byte) 0x31, (byte) 0x1A, (byte) 0x90, (byte) 0xD0, (byte) 0xA8, (byte) 0x44, (byte) 0x84, (byte) 0x73, (byte) 0x01, (byte) 0x1F, (byte) 0x51, (byte) 0x19, (byte) 0x43, (byte) 0x35, (byte) 0x2D, (byte) 0x3D, (byte) 0xC6, (byte) 0x2B, (byte) 0x68, (byte) 0x7C, (byte) 0x4F, (byte) 0x02, (byte) 0x03, (byte) 0x01, (byte) 0x00, (byte) 0x01 }; /\*\* \* Name of the property containing the encoded rights for this user/device/application. (BASE64) \*/ private final static String RIGHTS_BLOB_PROPERTY = "jnlp.javastore.appwrapper.rightsBlob"; /\*\* \* Name of the property containing the digital signature of the encoded rights object. (BASE64) \*/ private final static String RIGHTS_SIGNATURE_PROPERTY = "jnlp.javastore.appwrapper.rightsSig"; /\*\* \* Name of the property containing certificate of the signer of the encoded rights object. (BASE64) \*/ private final static String RIGHTS_CERTIFICATE_PROPERTY = "jnlp.javastore.appwrapper.rightsCert"; /\*\* \* The ID of the user running the application as provided by the Java Store wrapper. \*/ private final static String RIGHTS_USER_ID = "jnlp.javastore.appwrapper.UserID"; /\*\* \* The Product ID of this application as provided by the Java Store wrapper. \*/ private final static String RIGHTS_PRODUCT_ID = "jnlp.javastore.appwrapper.ProductID"; /\*\* \* The Version ID of this application as provided by the Java Store wrapper. \*/ private final static String RIGHTS_VERSION_ID = "jnlp.javastore.appwrapper.VersionID"; /\*\* \* The system fingerprint for this system as provided by the Java Store wrapper. \*/ private final static String RIGHTS_SYSTEM_ID = "jnlp.javastore.appwrapper.SystemID"; /\*\* \* Clock drift window. This is padding we add in time/date calculations to allow for differences between \* the user's clock and the Java Store clock. \*/ private final static long MAX_CLOCK_DRIFT_WINDOW_MS = /\*1sec\*/ 1000L \* /\*1min\*/ 60L \* /\*1hr\*/ 60L \* /\*1/2day\*/ 12L; /\*\* \* The basic right for purchased applications. If this right is absent or unavailable then the application \* is in demo mode. \*/ private final static String BASIC_PURCHASED_RIGHT = "store:run"; /\*\* \* Main entry point. \* \* @param args command line arguments (if any). \*/ public static void main(String[] args) { // We will decide if this application should be running in demo mode or not. boolean demoMode = true; try { // Get the current time. long now = System.currentTimeMillis(); // Recover rights info objects from system properties and decode BASE64 data. byte[] rightsBlob = decodeBase64(System.getProperty(RIGHTS_BLOB_PROPERTY, "")); byte[] rightsSig = decodeBase64(System.getProperty(RIGHTS_SIGNATURE_PROPERTY, "")); byte[] rightsCert = decodeBase64(System.getProperty(RIGHTS_CERTIFICATE_PROPERTY, "")); // Ensure that all of the rights info components are available. if (rightsBlob.length <= 0 && rightsSig.length <= 0 && rightsCert.length <= 0) { throw new IllegalArgumentException("Rights objects unavailable."); } // Read in the certificate of the signer. CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509Certificate certificate = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream( rightsCert)); // verify that the public key in the presented signer certificate matches the one know to trust. if (!Arrays.equals(TRUSTED_PUBLIC_KEY, certificate.getPublicKey().getEncoded())) { throw new IllegalArgumentException("Incorrect signer."); } // Verify the signature using the certificate against the rights info blob. Signature signature = Signature.getInstance("SHA1withRSA"); signature.initVerify(certificate); signature.update(rightsBlob); if (!signature.verify(rightsSig)) { throw new IllegalArgumentException("Signature doesn't match."); } // We can now trust that the rights info blob is authentic. // The rest of the processing is devoted to using the rights info. // Read a Properties map from the rights info blob. Properties rights = new Properties(); rights.load(new ByteArrayInputStream(rightsBlob)); // Extract some values from the properties map // The ID of the product to which the rights are applied. int productId = Integer.parseInt(rights.getProperty("productId", "-1")); rights.remove("productId"); // The ID of the user holding the rights. int userId = Integer.parseInt(rights.getProperty("userId", "-1")); rights.remove("userId"); int systemId = Integer.parseInt(rights.getProperty("systemId", "-1")); rights.remove("systemId"); // The ID of the device on which the application is running. String deviceId = rights.getProperty("deviceId", ""); rights.remove("deviceId"); // Timestamp at which this rights info was generated in milliseconds since Midnight Jan 1, 1970 UTC ("the epoch"). long timestamp = Long.parseLong(rights.getProperty("timestamp")); rights.remove("timestamp"); // Print out the core properties of the rights info. System.out.printf("product : %d user : %d system : %d device : '%s' timestamp : %s \\n", productId, userId, systemId, deviceId, new Date(timestamp)); // Perform checks upon the properties. // The product ID must be a positive number. int appProductId = Integer.parseInt(System.getProperty(RIGHTS_PRODUCT_ID, "-99")); if (productId != appProductId) { throw new IllegalArgumentException("Invalid product Id"); } // Normally the userID is > 0. If the userID is -1 then this is a preview or demo and we should // not expect any rights. int currentUserId = Integer.parseInt(System.getProperty(RIGHTS_USER_ID, "-99")); if (userId != currentUserId) { throw new IllegalArgumentException("Invalid user Id"); } // The ID of the system on which the application is running. int currentSystemId = Integer.parseInt(System.getProperty(RIGHTS_SYSTEM_ID, "0")); if (systemId != currentSystemId) { throw new IllegalArgumentException("Non-matching system Id"); } // Device ID must be present. This is used to track how many devices the user is installing the // application on. if (0 == deviceId.trim().length()) { throw new IllegalArgumentException("Invalid device Id"); } // Compare timestamp to current time allowing for clock drift. if ((timestamp < 0) || (now - timestamp < -MAX_CLOCK_DRIFT_WINDOW_MS)) { throw new IllegalArgumentException("Invalid time stamp"); } if (!rights.isEmpty()) { System.out.println(); } // At this point all remaining keys in the rights map should be single right descriptors. if ((-1 == userId) && !rights.isEmpty()) { System.out.printf("\*\*\* Rights Info should be empty when in demo/preview mode.\\n\\n"); } // Print out properties for each right in the rights info. for (Map.Entry aRight : rights.entrySet()) { String key = aRight.getKey().toString(); String value = aRight.getValue().toString(); // Not a right entry. if (!key.startsWith("right.")) { System.out.printf("\\t\*\* Unexpected property \*\* :: key : '%s' value : '%s'\\n", key, value); continue; } // Process a single right descriptor. Value is a BASE64 encoded property map. Properties right = new Properties(); right.load(new ByteArrayInputStream(decodeBase64(value))); // Read the properties for this right from the property map. // the name of the right (not unique) String name = right.getProperty("name"); // receipt id of the Java Store purchase transaction which acquired this right. (unique) int receiptId = Integer.parseInt(right.getProperty("receiptId")); // payment processor name. String paymentProcessor = right.getProperty("paymentProcessor"); // transaction id from the payment service (unique per payment procesor) String paymentTransactionId = right.getProperty("paymentTransactionId"); // if true then right is currently granted otherwise false (refunded, revoked, etc.) boolean isGranted = Boolean.parseBoolean(right.getProperty("isGranted", "false")); // timestamp in millis since epoch at which the right becomes (or beacame) active. Not present for perpetual rights. long activation = Long.parseLong(right.getProperty("activation", "-9223372036854775808")); // timestamp in millis since epoch at which the right expires. Not present for for perpetual rights. long expiration = Long.parseLong(right.getProperty("expiration", "9223372036854775807")); // Calculate whether this right is currently available. Application functionality for // absent and unavailable rights should be disabled. boolean available = isGranted && // must be granted. (now + MAX_CLOCK_DRIFT_WINDOW_MS >= activation) && // and activated. (now - MAX_CLOCK_DRIFT_WINDOW_MS < expiration) && // and not expired. (userId > 0) && // for a specific user (not preview or demo). (null != name); // and not null; // print out properties for this specific right. System.out.printf("\\tright : '%s' receipt id : %d payment : %s '%s' granted: %b \\n" + "\\t\\tTimestamps: %s (activation) %s (expiration)\\n" + "\\t\\t==> %s\\n", name, receiptId, paymentProcessor, paymentTransactionId, isGranted, (Long.MIN_VALUE == activation) ? "perpetual" : new Date(activation), (Long.MAX_VALUE == expiration) ? "perpetual" : new Date(expiration), available ? "available" : "unavailable"); // See if we should disable demo mode based upon this right. if (available && BASIC_PURCHASED_RIGHT.equals(name)) { demoMode = false; } } } catch (Exception failure) { System.err.println("Failure processing Java Store Licensing Rights"); failure.printStackTrace(System.err); throw new IllegalStateException("Invalid Java Store Licensing Rights", failure); } // Print out our decision about whether we are running in demo mode. System.out.printf("\\nApplication is running in %s mode.\\n", demoMode ? "demo" : "purchased"); } /\*\* \* Decodes a BASE64 string into a byte array. \* \* @param encodedBASE64 A BASE64 encoded string with optional whitespace. \* @return the decoded bytes. \*/ private static byte[] decodeBase64(String encodedBASE64) { ByteArrayOutputStream bos = new ByteArrayOutputStream(encodedBASE64.length()); int index = 0; int values = 0; int charsToBytes = 0; int trailing = 0; while (index < encodedBASE64.length()) { char c = encodedBASE64.charAt(index++); int v; // 6 bits if ('A' <= c && c <= 'Z') { v = (c - 'A'); } else if ('a' <= c && c <= 'z') { v = (c - 'a') + 26; } else if ('0' <= c && c <= '9') { v = (c - '0') + 52; } else if (c == '+') { v = 62; } else if (c == '/') { v = 63; } else if (c == '=') { v = Integer.MAX_VALUE; } else if (Character.isWhitespace(c)) { continue; } else { throw new IllegalArgumentException("Bad character in input @ " + (index - 1)); } values <<= 6; charsToBytes++; if (Integer.MAX_VALUE != v) { if (0 != trailing) { throw new IllegalArgumentException("Bad character in input @ " + (index - 1)); } values |= v; } else { trailing++; if (trailing > 2) { throw new IllegalArgumentException("Bad character in input @ " + (index - 1)); } } if (0 == (charsToBytes % 4)) { bos.write((byte) (values >> 16)); if (trailing < 2) { bos.write((byte) (values >> 8)); } if (trailing < 1) { bos.write((byte) values); } values = 0; } } return bos.toByteArray(); } }

And a unit test which invokes the sample code.

Java Store License Rights Unit Test
package com.sun.javastore.licenserightsapp;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

/\*\*
 \* Unit test for simple App.
 \*/
public class AppTest
        extends TestCase {

    /\*\*
     \* Create the test case
     \*
     \* @param testName name of the test case
     \*/
    public AppTest(String testName) {
        super(testName);
    }

    /\*\*
     \* @return the suite of tests being tested
     \*/
    public static Test suite() {
        return new TestSuite(AppTest.class);
    }

    /\*\*
     \* Rigourous Test :-)
     \*/
    public void testApp() {
        String SAMPLE_RIGHTS_BLOB =
                "I1dlZCBTZXAgMTYgMTc6MzM6NDQgUERUIDIwMDkKc3lzdGVtSWQ9MzU5Nzg3MQpyaWdodC4yPUkx" +
                "ZGxaQ0JUWlhBZ01UWWdNVGM2TXpNNk5EUWdVRVJVSURJd01Ea0taWGh3YVhKaGRHbHZiajB4TWpV" +
                "ek1UVXhNakV5TkRFNENtNWhiV1U5YzNSdmNtVmNPbk4xWW5OamNtbHdkR2x2YmdwcGMwZHlZVzUw" +
                "WldROWRISjFaUXB5WldObGFYQjBTV1E5TWdwd1lYbHRaVzUwVkhKaGJuTmhZM1JwYjI1SlpEMHhN" +
                "alFLWVdOMGFYWmhkR2x2YmoweE1qVXpNVFEzTmpFeU5ERTRDbkJoZVcxbGJuUlFjbTlqWlhOemIz" +
                "STlVR0Y1VUdGc1FWQUsKcmlnaHQuMT1JMWRsWkNCVFpYQWdNVFlnTVRjNk16TTZORFFnVUVSVUlE" +
                "SXdNRGtLYm1GdFpUMXpkRzl5WlZ3NmNuVnVDbWx6UjNKaGJuUmxaRDEwY25WbENuSmxZMlZwY0hS" +
                "SlpEMHhDbkJoZVcxbGJuUlVjbUZ1YzJGamRHbHZia2xrUFRFeU13cHdZWGx0Wlc1MFVISnZZMlZ6" + 
                "YzI5eVBWQmhlVkJoYkVGUUNnXD1cPQp1c2VySWQ9MQpwcm9kdWN0SWQ9Mwp0aW1lc3RhbXA9MTI1" +
                "MzE0NzYyNDYwMwpkZXZpY2VJZD05MzIxRTAyMy1DRUEyLTQxQTItODM1Mi1ERUVFODYxMDA4OEMK";

        String SAMPLE_RIGHTS_SIG =
                "GHNXWheMgBv8NC/jR9rVuwufyLgtirKsFq7C8dWL4Uzb4+3pZmfoqNLrxbXiTErCP4F42UnkP8mF" +
                "i8sFsdO99xGCuaa1zQqZO9zXNScKBxzRlKcSuzIRDynnq0CN9Wz82ibeA+b5S7HpsQ7nUbHb6SEu" +
                "9BqjArTEc8wu7RCcTZo=";

        String SAMPLE_RIGHTS_CERT =
                "MIIE5jCCA86gAwIBAgIQXqXlgX7rylXBGhLidL9EhzANBgkqhkiG9w0BAQUFADCBgzEdMBsGA1UE" +
                "ChMUU3VuIE1pY3Jvc3lzdGVtcyBJbmMxHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdvcmsx" +
                "JTAjBgNVBAsTHENsYXNzIDIgT25TaXRlIFN1YnNjcmliZXIgQ0ExGjAYBgNVBAMTEU9iamVjdCBT" +
                "aWduaW5nIENBMB4XDTA5MDgyNzAwMDAwMFoXDTEyMDgyNjIzNTk1OVowbTEdMBsGA1UEChQUU3Vu" +
                "IE1pY3Jvc3lzdGVtcyBJbmMxITAfBgNVBAsUGENvcnBvcmF0ZSBPYmplY3QgU2lnbmluZzEQMA4G" +
                "A1UECxQHQ2xhc3MgQzEXMBUGA1UEAxQOU3VuIEphdmEgU3RvcmUwgZ8wDQYJKoZIhvcNAQEBBQAD" +
                "gY0AMIGJAoGBAJghrmyAl2/ercRZRFfMPX5Ie+LusqP3UwMSB2A9Xl3via+kboXjuNHxTYyMEOUa" +
                "b3DorVSWsWHF5UKDK7KxkW+w/g2vYKnVZIKPH+VHRXYAwUMpEDNSSVMOyNKYF39uj0Ec9uRvhBw5" +
                "pxeKMRqQ0KhEhHMBH1EZQzUtPcYraHxPAgMBAAGjggHtMIIB6TAJBgNVHRMEAjAAMA4GA1UdDwEB" +
                "/wQEAwIHgDBrBgNVHR8EZDBiMGCgXqBchlpodHRwOi8vb25zaXRlY3JsLnZlcmlzaWduLmNvbS9T" +
                "dW5NaWNyb3N5c3RlbXNJbmNDb3Jwb3JhdGVPYmplY3RTaWduaW5nQ2xhc3NDL0xhdGVzdENSTC5j" +
                "cmwwHwYDVR0jBBgwFoAUs0crgn5TtHPKuLsZt76BTQeVx+0wHQYDVR0OBBYEFJHwpfEC3ziinvN3" +
                "8dKlgVR5OrW1MDsGCCsGAQUFBwEBBC8wLTArBggrBgEFBQcwAYYfaHR0cDovL29uc2l0ZS1vY3Nw" +
                "LnZlcmlzaWduLmNvbTCBuQYDVR0gBIGxMIGuMDkGC2CGSAGG+EUBBxcCMCowKAYIKwYBBQUHAgEW" +
                "HGh0dHBzOi8vd3d3LnZlcmlzaWduLmNvbS9ycGEwcQYLYIZIAYb3AIN9nD8wYjAnBggrBgEFBQcC" +
                "ARYbaHR0cHM6Ly93d3cuc3VuLmNvbS9wa2kvY3BzMDcGCCsGAQUFBwICMCsWKU5vdCBWYWxpZGF0" +
                "ZWQgRm9yIFN1biBCdXNpbmVzcyBPcGVyYXRpb25zMBEGCWCGSAGG+EIBAQQEAwIEEDATBgNVHSUE" +
                "DDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQUFAAOCAQEAPoQ8RWJ6xfnY8YxHL3kEVEPxKyDAa+sG" +
                "Ng5Rw2SaXFek9KMfMvU6DUXdj/BYIuLu9S2cVIHlIyFINzT6gtxsDNTkByMmRys8lsCZhXv767Pe" +
                "b/KW2sZWA8fxSE6JDRpWsE7j4PSr3rBB6YNqCTX0qH5yzumx2Lfk28BILKW7ri4cmakaql+Vi+6q" +
                "QIqhxDu1FzAdbwp/DzAJYtafcAZa381jDH3kwpSXpP15V1jqZgGYs387hQEYDWie4J5sol7g0cN9" +
                "zlRZQMxwU6Rcz/nWiM24mGB/W0NH7ggMS7/AWz6efN534OAxgWuBA96Qwf7TWKdZWzWEdhZar483" +
                "hfmZiw==";

        System.setProperty("jnlp.javastore.appwrapper.UserID", "1");
        System.setProperty("jnlp.javastore.appwrapper.ProductID", "3");
        System.setProperty("jnlp.javastore.appwrapper.VersionID", "3");

        // This value will be calculated by the java store runtime.
        int systemId = "user".hashCode() \^ "os".hashCode();

        System.setProperty("jnlp.javastore.appwrapper.SystemID", Integer.toString(systemId));

        System.setProperty("jnlp.javastore.appwrapper.rightsBlob", SAMPLE_RIGHTS_BLOB);
        System.setProperty("jnlp.javastore.appwrapper.rightsSig", SAMPLE_RIGHTS_SIG);
        System.setProperty("jnlp.javastore.appwrapper.rightsCert", SAMPLE_RIGHTS_CERT);

        App.main(new String[0]);
    }
}
About

mduigou

Search

Categories
Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today