All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.schlichtherle.license.LicenseManager Maven / Gradle / Ivy

There is a newer version: 1.33
Show newest version
/*
 * Copyright (C) 2005-2015 Schlichtherle IT Services.
 * All rights reserved. Use is subject to license terms.
 */
package de.schlichtherle.license;

import de.schlichtherle.util.ObfuscatedString;
import de.schlichtherle.xml.GenericCertificate;
import java.io.*;
import java.util.Calendar;
import java.util.Date;
import java.util.prefs.Preferences;
import javax.security.auth.x500.X500Principal;
import javax.swing.filechooser.FileFilter;

/**
 * This is the top level class which manages all licensing aspects like for
 * instance the creation, installation and verification of license keys.
 * The license manager knows how to install, verify and uninstall full and
 * trial licenses for a given subject and ensures the privacy of the license
 * content in its persistent form (i.e. the license key).
 * For signing, verifying and validating licenses, this class cooperates with
 * a {@link LicenseNotary}.
 * 

* This class is thread-safe. * * @author Christian Schlichtherle * @version $Id$ */ public class LicenseManager implements LicenseCreator, LicenseVerifier { /** The timeout for the license content cache. */ private static final long TIMEOUT = 30 * 60 * 1000; // half an hour /** * The key in the preferences used to store the license key. * * => "license" */ private static final String PREFERENCES_KEY = new ObfuscatedString(new long[] { 0xD65FA96737AE2CB5L, 0xE804D1A38CF9A413L }).toString(); /** * The suffix for files which hold license certificates. * * => ".lic" - must be lowercase! */ public static final String LICENSE_SUFFIX = new ObfuscatedString(new long[] { 0x97187B3A07E79CEEL, 0x469144B7E0D475E2L }).toString(); static { assert LICENSE_SUFFIX.equals(LICENSE_SUFFIX.toLowerCase()); // paranoid } /** => "CN=" */ protected static final String CN = new ObfuscatedString(new long[] { 0x636F59E1FF007F64L, 0xAC9CE58690A43DD0L }).toString(); /** => "user" */ private static final String CN_USER = CN + Resources.getString( new ObfuscatedString(new long[] { 0xF3BE4EA2CCDD7EADL, 0x5B6A9F59A1183108L }).toString()); /** => "User" */ private static final String USER = new ObfuscatedString(new long[] { 0x9F89522C9F6F4A13L, 0xFFDB7A316241AC79L }).toString(); /** => "System" */ private static final String SYSTEM = new ObfuscatedString(new long[] { 0xEC006BE1C1F75BD6L, 0x54D650CDD244774BL }).toString(); /** => "exc.invalidSubject" */ private static final String EXC_INVALID_SUBJECT = new ObfuscatedString(new long[] { 0x8029CDF4E32A76ECL, 0x56FA623D9AEE8C1L, 0x99E7882A708663ACL, 0x5888C0D72E548FF4L }).toString(); /** => "exc.holderIsNull" */ private static final String EXC_HOLDER_IS_NULL = new ObfuscatedString(new long[] { 0x6339FEFCDFD84427L, 0x57A2FA0735E47CBEL, 0xED1D06E6EED72950L }).toString(); /** => "exc.issuerIsNull" */ private static final String EXC_ISSUER_IS_NULL = new ObfuscatedString(new long[] { 0xD5E29AC879334756L, 0xF1F7421CD6A06536L, 0x5E086D6468FECBF2L }).toString(); /** => "exc.issuedIsNull" */ private static final String EXC_ISSUED_IS_NULL = new ObfuscatedString(new long[] { 0xAB8FF89F2DA6C32CL, 0x2A089A9CA80D970EL, 0xCF15F8842FCCD9D5L }).toString(); /** => "exc.licenseIsNotYetValid" */ private static final String EXC_LICENSE_IS_NOT_YET_VALID = new ObfuscatedString(new long[] { 0x4B6BB2804EE7DDB1L, 0xD0BB0A33A41543C5L, 0x5FCEC6DF3725CEE4L, 0xA165775BBD625344L }).toString(); /** => "exc.licenseHasExpired" */ private static final String EXC_LICENSE_HAS_EXPIRED = new ObfuscatedString(new long[] { 0xDE2B2A7ACD6DA6DL, 0x9EE12DDECB3D4C0DL, 0xB3CF760B522E8688L, 0x316BD3E92C17CC40L }).toString(); /** => "exc.consumerTypeIsNull" */ private static final String EXC_CONSUMER_TYPE_IS_NULL = new ObfuscatedString(new long[] { 0xD29019F7B1D95C66L, 0xE859C44ACC3EB2FEL, 0xF041027C9003B031L, 0x27E84AD8870D6063L }).toString(); /** => "exc.consumerTypeIsNotUser" */ private static final String EXC_CONSUMER_TYPE_IS_NOT_USER = new ObfuscatedString(new long[] { 0xCE99D49CE98D1E47L, 0x7A3BA300A7DFCEABL, 0x2D2E4B624AD7C4E0L, 0x2C86A28A075E71C6L, 0x79BCB920E5FB351DL }).toString(); /** => "exc.consumerAmountIsNotOne" */ private static final String EXC_CONSUMER_AMOUNT_IS_NOT_ONE = new ObfuscatedString(new long[] { 0x5F20CBB98126BB0AL, 0xE8BB696B25D24011L, 0x435CC3AA7263BAE7L, 0x9DA3066F501717E4L, 0x62FFA4899FBBA3F8L }).toString(); /** => "exc.consumerAmountIsNotPositive" */ private static final String EXC_CONSUMER_AMOUNT_IS_NOT_POSITIVE = new ObfuscatedString(new long[] { 0xB14EB6259B4D7249L, 0xCD02F577511528D8L, 0x39B8CF1E258756DDL, 0x67488F05891DF916L, 0x4256DE0CFFF62DCAL }).toString(); /** => "fileFilter.description" */ private static final String FILE_FILTER_DESCRIPTION = new ObfuscatedString(new long[] { 0x2BDDE408C7B71604L, 0xDFCA7DA8912DE4C1L, 0xADA1FC1C1D5F1047L, 0xD08EAA6CCDC342F3L }).toString(); /** => " (*.lic)" */ private static final String FILE_FILTER_SUFFIX = new ObfuscatedString(new long[] { 0xA4BCC907D9FD1290L, 0x614A0A9015D3D8DDL }).toString(); /** Returns midnight local time today. */ protected static Date midnight() { Calendar cal = Calendar.getInstance(); cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0); return cal.getTime(); } private LicenseParam param; // initialized by setLicenseParam() - should be accessed via getLicenseParam() only! // // Data computed and cached from the license configuration parameters. // private LicenseNotary notary; // lazy initialized private PrivacyGuard guard; // lazy initialized /** The cached certificate of the current license key. */ private GenericCertificate certificate; // lazy initialized /** The time when the certificate was last set. */ private long certificateTime; // lazy initialized /** A suitable file filter for the subject of this license manager. */ private FileFilter fileFilter; // lazy initialized /** The preferences node used to store the license key. */ private Preferences preferences; // lazy initialized /** * Constructs a License Manager. *

* Warning: The manager created by this constructor is not * valid and cannot be used unless {@link #setLicenseParam(LicenseParam)} * is called! */ protected LicenseManager() { } /** * Constructs a License Manager. * * @param param the license configuration parameters * - may not be {@code null}. * @throws NullPointerException If the given parameter object does not * obey the contract of its interface due to a {@code null} * pointer. * @throws IllegalPasswordException If any password in the parameter object * does not comply to the current policy. */ public LicenseManager(LicenseParam param) { setLicenseParam0(param); } /** Returns the license configuration parameters. */ public synchronized LicenseParam getLicenseParam() { return param; } /** * Sets the license configuration parameters. * Calling this method resets the manager as if it had been * newly created. * Some plausibility checks are applied to the given parameter object * to ensure that it adheres to the contract of the parameter interfaces. * * @param param the license configuration parameters * - may not be {@code null}. * @throws NullPointerException If the given parameter object does not * obey the contract of its interface due to a {@code null} * pointer. * @throws IllegalPasswordException If any password in the parameter object * does not comply to the current policy. */ public synchronized void setLicenseParam(LicenseParam param) { setLicenseParam0(param); } private void setLicenseParam0(LicenseParam param) { // Check parameters to implement fail-fast behaviour. final CipherParam cipherParam; if (null == param || null == param.getSubject() || null == param.getKeyStoreParam() || null == (cipherParam = param.getCipherParam())) throw new NullPointerException(); //if (null == param.getPreferences()) // throw new NullPointerException(); Policy.getCurrent().checkPwd(cipherParam.getKeyPwd()); this.param = param; notary = null; certificate = null; certificateTime = 0; fileFilter = null; preferences = null; } // // Methods for license contents. // /** * Initializes and validates the license content, creates a new signed * license certificate for it and compresses, encrypts and stores it to * the given file as a license key. *

* As a side effect, the given license {@code content} is initialized * with some reasonable defaults unless the respective properties have * already been set. * * @param content the license content * - may not be {@code null}. * @param keyFile the file to save the license key to * - may not be {@code null}. * This should have a {@code LICENSE_SUFFIX}. * @throws Exception for various reasons. Note that you should always use * {@link Throwable#getLocalizedMessage()} to get a (possibly * localized) meaningful detail message. * * @see #store(LicenseContent, LicenseNotary, File) * @see #initialize(LicenseContent) * @see #validate(LicenseContent) */ public final synchronized void store(LicenseContent content, File keyFile) throws Exception { store(content, getLicenseNotary(), keyFile); } /** * Initializes and validates the license content, creates a new signed * license certificate for it and compresses, encrypts and stores it to * the given file as a license key. *

* As a side effect, the given license {@code content} is initialized * with some reasonable defaults unless the respective properties have * already been set. * * @param content the license content * - may not be {@code null}. * @param notary the license notary used to sign the license key * - may not be {@code null}. * @param keyFile the file to save the license key to * - may not be {@code null}. * This should have a {@code LICENSE_SUFFIX}. * @throws Exception for various reasons. * Note that you should always use * {@link Throwable#getLocalizedMessage()} to get a (possibly * localized) meaningful detail message. * @see #initialize(LicenseContent) * @see #validate(LicenseContent) */ protected synchronized void store( LicenseContent content, LicenseNotary notary, File keyFile) throws Exception { storeLicenseKey(create(content, notary), keyFile); } /** * Initializes and validates the license content, creates a new signed * license certificate for it and compresses, encrypts and returns it * as a license key. *

* As a side effect, the given license {@code content} is initialized * with some reasonable defaults unless the respective properties have * already been set. * * @param content the license content * - may not be {@code null}. * @throws Exception for various reasons. * Note that you should always use * {@link Throwable#getLocalizedMessage()} to get a (possibly * localized) meaningful detail message. * @return The license key * - {@code null} is never returned. * @see #create(LicenseContent, LicenseNotary) * @see #initialize(LicenseContent) * @see #validate(LicenseContent) */ public final synchronized byte[] create(LicenseContent content) throws Exception { return create(content, getLicenseNotary()); } /** * Initializes and validates the license content, creates a new signed * license certificate for it and compresses, encrypts and returns it * as a license key. *

* As a side effect, the given license {@code content} is initialized * with some reasonable defaults unless the respective properties have * already been set. * * @param content the license content * - may not be {@code null}. * @param notary the license notary used to sign the license key * - may not be {@code null}. * @throws Exception for various reasons. * Note that you should always use * {@link Throwable#getLocalizedMessage()} to get a (possibly * localized) meaningful detail message. * @return The license key * - {@code null} is never returned. * @see #initialize(LicenseContent) * @see #validate(LicenseContent) */ protected synchronized byte[] create( LicenseContent content, LicenseNotary notary) throws Exception { initialize(content); validate(content); final GenericCertificate certificate = notary.sign(content); final byte[] key = getPrivacyGuard().cert2key(certificate); return key; } /** * Loads, decrypts, decompresses, decodes and verifies the license key in * {@code keyFile}, validates its license content and installs it * as the current license key. * * @param keyFile the file to load the license key from * - may not be {@code null}. * @throws Exception for various reasons. * Note that you should always use * {@link Throwable#getLocalizedMessage()} to get a (possibly * localized) meaningful detail message. * @return A clone of the verified and validated content of the license key * - {@code null} is never returned. * @see #install(File, LicenseNotary) * @see #validate(LicenseContent) */ public final synchronized LicenseContent install(File keyFile) throws Exception { return install(keyFile, getLicenseNotary()); } /** * Loads, decrypts, decompresses, decodes and verifies the license key in * {@code keyFile}, validates its license content and installs it * as the current license key. * * @param keyFile The file to load the license key from * - may not be {@code null}. * @param notary The license notary used to verify the license key * - may not be {@code null}. * @throws Exception for various reasons. * Note that you should always use * {@link Throwable#getLocalizedMessage()} to get a (possibly * localized) meaningful detail message. * @return A clone of the verified and validated content of the license key * - {@code null} is never returned. * @see #validate(LicenseContent) */ protected synchronized LicenseContent install( File keyFile, LicenseNotary notary) throws Exception { return install(loadLicenseKey(keyFile), notary); } /** * Decrypts, decompresses, decodes and verifies the license key in * {@code key}, validates its license content and installs it * as the current license key. * * @param key the license key * - may not be {@code null}. * @param notary the license notary used to verify the license key * - may not be {@code null}. * @throws Exception for various reasons. * Note that you should always use * {@link Throwable#getLocalizedMessage()} to get a (possibly * localized) meaningful detail message. * @return A clone of the verified and validated content of the license key * - {@code null} is never returned. * @see #validate(LicenseContent) */ protected synchronized LicenseContent install( final byte[] key, final LicenseNotary notary) throws Exception { final GenericCertificate certificate = getPrivacyGuard().key2cert(key); notary.verify(certificate); final LicenseContent content = (LicenseContent) certificate.getContent(); validate(content); setLicenseKey(key); setCertificate(certificate); return content; } /** * Decrypts, decompresses, decodes and verifies the current license key, * validates its license content and returns it. * * @throws NoLicenseInstalledException if no license key is installed. * @throws Exception for any other reason. * Note that you should always use * {@link Throwable#getLocalizedMessage()} to get a (possibly * localized) meaningful detail message. * @return A clone of the verified and validated content of the license key * - {@code null} is never returned. * @see #validate(LicenseContent) */ public final synchronized LicenseContent verify() throws Exception { return verify(getLicenseNotary()); } /** * Decrypts, decompresses, decodes and verifies the current license key, * validates its license content and returns it. * * @param notary the license notary used to verify the current license key * - may not be {@code null}. * @throws NoLicenseInstalledException if no license key is installed. * @throws Exception for any other reason. * Note that you should always use * {@link Throwable#getLocalizedMessage()} to get a (possibly * localized) meaningful detail message. * @return A clone of the verified and validated content of the license key * - {@code null} is never returned. * @see #validate(LicenseContent) */ protected synchronized LicenseContent verify(final LicenseNotary notary) throws Exception { GenericCertificate certificate = getCertificate(); if (null != certificate) return (LicenseContent) certificate.getContent(); // Load license key from preferences, final byte[] key = getLicenseKey(); if (null == key) throw new NoLicenseInstalledException(getLicenseParam().getSubject()); certificate = getPrivacyGuard().key2cert(key); notary.verify(certificate); final LicenseContent content = (LicenseContent) certificate.getContent(); validate(content); setCertificate(certificate); return content; } /** * Decrypts, decompresses, decodes and verifies the given license key, * validates its license content and returns it. * * @param key the license key * - may not be {@code null}. * @throws Exception an instance of a subclass of this class for various * reasons. * Note that you should always use * {@link Throwable#getLocalizedMessage()} to get a (possibly * localized) meaningful detail message. * @return A clone of the verified and validated content of the license key * - {@code null} is never returned. * @see #validate(LicenseContent) */ public final synchronized LicenseContent verify(byte[] key) throws Exception { return verify(key, getLicenseNotary()); } /** * Decrypts, decompresses, decodes and verifies the given license key, * validates its license content and returns it. * * @param key the license key * - may not be {@code null}. * @param notary the license notary used to verify the license key * - may not be {@code null}. * @throws Exception for various reasons. * Note that you should always use * {@link Throwable#getLocalizedMessage()} to get a (possibly * localized) meaningful detail message. * @return A clone of the verified and validated content of the license key * - {@code null} is never returned. * @see #validate(LicenseContent) */ protected synchronized LicenseContent verify( final byte[] key, final LicenseNotary notary) throws Exception { final GenericCertificate certificate = getPrivacyGuard().key2cert(key); notary.verify(certificate); final LicenseContent content = (LicenseContent) certificate.getContent(); validate(content); return content; } /** * Uninstalls the current license key. * * @throws Exception An instance of a subclass of this class for various * reasons. */ public synchronized void uninstall() throws Exception { setLicenseKey(null); setCertificate(null); } /** * Initializes the given license {@code content} with some reasonable * defaults unless the respective properties have already been set. * * @see #validate(LicenseContent) */ protected synchronized void initialize(final LicenseContent content) { if (content.getHolder() == null) content.setHolder(new X500Principal(CN_USER)); if (content.getSubject() == null) content.setSubject(getLicenseParam().getSubject()); if (content.getConsumerType() == null) { final Preferences prefs = getLicenseParam().getPreferences(); if (prefs != null) { if (prefs.isUserNode()) content.setConsumerType(USER); else content.setConsumerType(SYSTEM); content.setConsumerAmount(1); } } if (content.getIssuer() == null) content.setIssuer(new X500Principal( CN + getLicenseParam().getSubject())); if (content.getIssued() == null) content.setIssued(new Date()); if (content.getNotBefore() == null) content.setNotBefore(midnight()); } /** * Validates the license content. * This method is called whenever a license certificate is created, * installed or verified. *

* Validation consists of the following plausability checks for the * properties of this class: *

*

    *
  • 'subject' must match the subject required by the application * via the {@link LicenseParam} interface. *
  • 'holder', 'issuer' and 'issued' must be provided (i.e. not * {@code null}). *
  • If 'notBefore' or 'notAfter' are provided, the current date and * time must match their restrictions. *
  • 'consumerType' must be provided and 'consumerAmount' must be * positive. If a user preference node is provided in the license * parameters, 'consumerType' must also match {@code "User"} (whereby * case is ignored) and 'consumerAmount' must equal 1. *
*

* If you need more or less rigid restrictions, you should override this * method in a subclass. * * @param content the license content * - may not be {@code null}. * @throws NullPointerException if {@code content} is {@code null}. * @throws LicenseContentException if any validation test fails. * Note that you should always use * {@link Throwable#getLocalizedMessage()} to get a (possibly * localized) meaningful detail message. * @see #initialize(LicenseContent) */ protected synchronized void validate(final LicenseContent content) throws LicenseContentException { final LicenseParam param = getLicenseParam(); if (!param.getSubject().equals(content.getSubject())) throw new LicenseContentException(EXC_INVALID_SUBJECT); if (content.getHolder() == null) throw new LicenseContentException(EXC_HOLDER_IS_NULL); if (content.getIssuer() == null) throw new LicenseContentException(EXC_ISSUER_IS_NULL); if (content.getIssued() == null) throw new LicenseContentException(EXC_ISSUED_IS_NULL); final Date now = new Date(); final Date notBefore = content.getNotBefore(); if (notBefore != null && now.before(notBefore)) throw new LicenseContentException(EXC_LICENSE_IS_NOT_YET_VALID); final Date notAfter = content.getNotAfter(); if (notAfter != null && now.after(notAfter)) throw new LicenseContentException(EXC_LICENSE_HAS_EXPIRED); final String consumerType = content.getConsumerType(); if (consumerType == null) throw new LicenseContentException(EXC_CONSUMER_TYPE_IS_NULL); final Preferences prefs = param.getPreferences(); if (prefs != null && prefs.isUserNode()) { if (!USER.equalsIgnoreCase(consumerType)) throw new LicenseContentException(EXC_CONSUMER_TYPE_IS_NOT_USER); if (content.getConsumerAmount() != 1) throw new LicenseContentException(EXC_CONSUMER_AMOUNT_IS_NOT_ONE); } else { if (content.getConsumerAmount() <= 0) throw new LicenseContentException(EXC_CONSUMER_AMOUNT_IS_NOT_POSITIVE); } } // // Methods for license certificates. // /** * Returns the license certificate cached from the * last installation/verification of a license key * or {@code null} if there wasn't an installation/verification * or a timeout has occured. */ protected synchronized GenericCertificate getCertificate() { if (null != certificate && System.currentTimeMillis() < certificateTime + TIMEOUT) return certificate; // use cached certificate until timeout else return null; } /** * Sets the given license certificate as installed or verified. * * @param certificate the license certificate * - may be {@code null} to clear. */ protected synchronized void setCertificate(GenericCertificate certificate) { this.certificate = certificate; certificateTime = System.currentTimeMillis(); // set cache timeout } // // Methods for license keys. // Note that in contrast to the methods of the privacy guard, // the following methods may have side effects (preferences, file system). // /** * Returns the current license key. */ protected synchronized byte[] getLicenseKey() { return getLicenseParam().getPreferences().getByteArray(PREFERENCES_KEY, null); } /** * Installs the given license key as the current license key. * If {@code key} is {@code null}, the current license key gets * uninstalled (but the cached license certificate is not cleared). */ protected synchronized void setLicenseKey(final byte[] key) { final Preferences prefs = getLicenseParam().getPreferences(); if (key != null) prefs.putByteArray(PREFERENCES_KEY, key); else prefs.remove(PREFERENCES_KEY); } /** * Stores the given license key to the given file. * * @param key the license key * - may not be {@code null}. * @param keyFile the file to save the license key to * - may not be {@code null}. * This should have a {@code LICENSE_SUFFIX}. */ protected static void storeLicenseKey( final byte[] key, final File keyFile) throws IOException { final OutputStream out = new FileOutputStream(keyFile); try { out.write(key); } finally { try { out.close(); } catch (IOException weDontCare) { } } } /** * Loads and returns the first megabyte of content from {@code keyFile} * as license key in a newly created byte array. * * @param keyFile the file holding the license key * - may not be {@code null}. */ protected static byte[] loadLicenseKey(final File keyFile) throws IOException { // Allow max 1MB size files and let the verifier detect a partial read final int size = Math.min((int) keyFile.length(), 1024 * 1024); final byte[] b = new byte[size]; final InputStream in = new FileInputStream(keyFile); try { // Let the verifier detect a partial read as an error in.read(b); } finally { in.close(); } return b; } // // Various stuff. // /** * Returns a license notary configured to use the keystore parameters * contained in the current license parameters * - {@code null} is never returned. */ protected synchronized LicenseNotary getLicenseNotary() { if (notary == null) notary = new LicenseNotary(getLicenseParam().getKeyStoreParam()); return notary; } /** * Returns a privacy guard configured to use the cipher parameters * contained in the current license parameters * - {@code null} is never returned. */ protected synchronized PrivacyGuard getPrivacyGuard() { if (guard == null) guard = new PrivacyGuard(getLicenseParam().getCipherParam()); return guard; } /** * Returns a suitable file filter for the subject of this license manager. * On Windows systems, the case of the suffix is ignored when browsing * directories. * * @return A valid {@code FileFilter}. */ public synchronized FileFilter getFileFilter() { if (fileFilter != null) return fileFilter; final String description = Resources.getString(FILE_FILTER_DESCRIPTION, getLicenseParam().getSubject()); if (File.separatorChar == '\\') { fileFilter = new FileFilter() { public boolean accept(File f) { return f.isDirectory() || f.getPath().toLowerCase().endsWith(LICENSE_SUFFIX); } public String getDescription() { return description + FILE_FILTER_SUFFIX; } }; } else { fileFilter = new FileFilter() { public boolean accept(File f) { return f.isDirectory() || f.getPath().endsWith(LICENSE_SUFFIX); } public String getDescription() { return description + FILE_FILTER_SUFFIX; } }; } return fileFilter; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy