Java Store and license management

The Java Store that was previewed during Java One earlier this year is getting ready to release support for paid applications. Developers who are getting ready to publish apps to the store should be aware of the license rights management facility. Here are the requirements in a nutshell:

  • Is transparent to users
  • Ensures users stay within their usage rights
  • Ensures that simple script based attacks to remove the rights management code aren't feasible
  • Eliminates the need to have an online connection when the application is used
Here is some sample code on how to use license management in your application:


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.Map;
import java.util.Properties;

/\*\*
 \* A sample application demonstrating how to decode and verify the Java Store rights object.
 \*
 \*/
public class App {

    public static void main(String[] args) throws Exception {

        String sample =
                // blob
                "...⁞";

        // The rights object is provided as a single string containing 3 sections separated by carriage
        // returns. First is the blob describing the rights, second is the signature for the blob and
        // third is the certificate matching the private key used to generate the signature.
        String[] BASE64pieces = sample.split("\\n");

        if (3 != BASE64pieces.length) {
            throw new IllegalArgumentException("invalid signed blob.");
        }

        byte[][] pieces = {null, null, null};

        // each piece is encoded as a separate BASE64 block.
        for (int eachPiece = 0; eachPiece < BASE64pieces.length; eachPiece++) {
            pieces[eachPiece] = decodeBase64(BASE64pieces[eachPiece]);
        }

        // Read in the certificate.
        CertificateFactory cf = CertificateFactory.getInstance("X.509");

        X509Certificate certificate = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(
                pieces[2]));


        // Verify the signature in piece[1] using the certificate from piece[2] against the blob from piece[0]
        Signature signature = Signature.getInstance("SHA1withRSA");
        signature.initVerify(certificate);
        signature.update(pieces[0]);
        if (!signature.verify(pieces[1])) {
            throw new IllegalArgumentException("Signature doesn't match.");
        }

        // Read a Properties map from the rights blob.
        Properties rights = new Properties();
        rights.load(new ByteArrayInputStream(pieces[0]));

        // 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");

        // The ID of the device on which the application is running.
        String deviceId = rights.getProperty("deviceId", "");
        rights.remove("deviceId");

        // Timestamp at which this rights descriptor 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
        System.out.printf("product : %d user : %d device : '%s' timestamp : %d \\n", productId, userId, deviceId, timestamp);

        // Print out properties for each right. Remaining keys should all be single right descriptors.
        for (Map.Entry aRight : rights.entrySet()) {
            String key = aRight.getKey().toString();
            String value = aRight.getValue().toString();

            if(!key.startsWith("right.")) {
                System.out.printf("\\t\*\* Unexpected property \*\* :: key : '%s' value : '%s'\\n", key, value);
                continue;
            }

            // print out a single right descriptor. Value is a BASE64 encoded property map.
            Properties right = new Properties();
            right.load(new ByteArrayInputStream(decodeBase64(value)));

            // print out properties for this specific right.
            System.out.printf("\\tright : '%s' receiptId : %d paymentTransactionId : '%s' granted: %b \\n" +
                    "\\t\\tTimestamps: %d (creation) %d (termination)\\n ",
                    // the name of the right (not unique)
                    right.getProperty("name"),
                    // receipt id of the purchase transaction (unique)
                    Integer.parseInt(right.getProperty("receiptId")),
                    // transaction id from the payment service (unique)
                    right.getProperty("paymentTransactionId"),
                    // if true then right is currently granted otherwise false (refunded, revoked, expired, etc.)
                    Boolean.parseBoolean(right.getProperty("granted")),
                    // timestamp in millis since epoch at which the right becomes (or beacame) active. May be any value before the current time for perpetual rights.
                    Long.parseLong(right.getProperty("timestamp.creation")),
                    // timestamp in millis since epoch at which the right expires. May be Long.MAX_VALUE for perpetual rights.
                    Long.parseLong(right.getProperty("timestamp.terminal")));
        }
    }

    /\*\*
     \*  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());

        char[] chars = encodedBASE64.toCharArray();
        int index = 0;
        int values = 0;
        int charsToBytes = 0;
        int trailing = 0;

        while (index < chars.length) {
            char c = chars[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();
    }
}
Comments:

Post a Comment:
Comments are closed for this entry.
About

octav

Search

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