/*
* Copyright (c) 2003-onwards Shaven Puppy Ltd
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'Shaven Puppy' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.puppygames.gamecommerce.shared;
import java.io.Serializable;
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
/**
* A customer's registration details
*/
public final class RegistrationDetails implements Serializable {
/** Encoding for prefs */
private static final String ENCODING = "UTF-8";
private static final String OLD_ENCODING = "ISO-8859-1";
public static final long serialVersionUID = 1L;
/** The public key */
public static final String PUBLIC_KEY =
"308201b83082012c06072a8648ce3804013082011f02818100fd7f53811d75122952df4a9c2eece4e7f611b7523cef4400c31e3f80b6512669455d402251fb593d8d58fabfc5f5ba30f6cb9b556cd7813b801d346ff26660b76b9950a5a49f9fe8047b1022c24fbba9d7feb7c61bf83b57e7c6a8a6150f04fb83f6d3c51ec3023554135a169132f675f3ae2b61d72aeff22203199dd14801c70215009760508f15230bccb292b982a2eb840bf0581cf502818100f7e1a085d69b3ddecbbcab5c36b857b97994afbbfa3aea82f9574c0b3d0782675159578ebad4594fe67107108180b449167123e84c281613b7cf09328cc8a6e13c167a8b547c8d28e0a3ae1e2bb3a675916ea37f0bfa213562f1fb627a01243bcca4f1bea8519089a883dfe15ae59f06928b665e807b552564014c3bfecf492a0381850002818100cfff377f44e641902f83c8bd33af2f02efddbb53466d3b0a889f4f936fc88e1f8f39427944d35c04f6c7d6afa38a642d54faa18117a9aeac6f1f5e98b2d8dbdff60406aa11fd43962d052319d883cf361ed65b0b35453fcb37496be9e69448ce4445626629ada640b2f16d56c4c062252bca564087d920ec7805ac2520dd4620";
/** The key and value for date checking */
private static final String ENCRYPT_KEY = "name";
private static final int EXPIRY_DAYS = 7;
private static final String PASSWORD ="xyzzy";
/** THe name to use when the registration is unregistered */
public static final String UNREGISTERED_NAME = "Unregistered";
/** The algorithm used */
public static final String ALGORITHM = "DSA";
/** The algorithm used to do signatures */
private static final String SIGNATURE_ALGORITHM = "SHA1withDSA";
/** Game title */
private final String game;
/** Customer's name */
private String name;
/** Customer's address */
private String address;
/** Customer's email address */
private String email;
/** Customer's registration code */
private byte[] regCode;
/** Whether the key can be exported */
private boolean exportable;
/**
* Database interface
*/
public interface RegistrationDatabase {
/**
* Create a new registration record. The registration record will be in unregistered
* status awaiting the user to activate their copy.
* <p>
* This method is called by the private ecommerce server when a successful payment
* has been processed.
*
* @param regCode The HexEncoded registration key
* @param name The registered username
* @param address The registered address
* @param email The registered email address
* @param authCode The authorisation code (last 14 chars of the regcode)
* @param purchaseDate The purchase date
* @param game The game title
* @param orderNumber The order number from the payment processor
* @param configuration A serializable configuration object that can be encoded in toString()
* @throws SQLException
*/
public void insertRegistration(
String regcode,
String name,
String address,
String email,
String authcode,
java.sql.Date purchasedate,
String game,
String orderNumber,
ConfigurationDetails configuration
) throws SQLException;
}
/**
* C'tor; loads the registratoin details from a result set.
* @param rs Result set
*/
public RegistrationDetails(ResultSet rs) throws SQLException {
game = rs.getString("game");
regCode = HexDecoder.decode(rs.getString("regcode"));
name = rs.getString("name");
email = rs.getString("email");
address = rs.getString("address");
}
/**
* C'tor; loads the registration details from local preferences
* @param game The game title
*/
public RegistrationDetails(String game) throws Exception {
this.game = game;
Preferences prefs = Preferences.userNodeForPackage(RegistrationDetails.class).node(game);
name = prefs.get("name", UNREGISTERED_NAME);
address = prefs.get("address", "");
email = prefs.get("email", "");
String code = prefs.get("key", "unregistered");
if (code.equals("unregistered")) {
regCode = null;
} else {
regCode = HexDecoder.decode(code);
}
}
/**
* Constructor
*/
public RegistrationDetails(
String game,
String name,
String address,
String email
) {
this(game, name, address, email, null);
}
/**
* Constructor
*/
public RegistrationDetails(
String game,
String name,
String address,
String email,
String key
) {
this.game = game;
this.name = name;
this.address = address;
this.email = email;
this.regCode = HexDecoder.decode(key);
}
public void createNewRegistration(RegistrationDatabase db, String orderNo, String game, String authCode, java.sql.Date purchaseDate, ConfigurationDetails configuration) throws SQLException {
db.insertRegistration(HexEncoder.encode(regCode), name, address, email, authCode, purchaseDate, game, orderNo, configuration);
}
public void createNewRegistration(RegistrationDatabase db, String orderNo, String game, java.sql.Date purchaseDate, ConfigurationDetails configuration) throws SQLException {
db.insertRegistration(HexEncoder.encode(regCode), name, address, email, getAuthCode(), purchaseDate, game, orderNo, configuration);
}
/**
* @return an authorisation code
*/
public String getAuthCode() {
String packedRegCode = HexEncoder.encode(regCode);
return packedRegCode.substring(packedRegCode.length() - 14, packedRegCode.length());
}
/**
* Stash this in some preferences
*/
public void toPreferences() {
if (regCode == null) {
return;
}
Preferences prefs = Preferences.userNodeForPackage(RegistrationDetails.class).node(game);
prefs.put("name", name);
prefs.put("address", address);
prefs.put("email", email);
prefs.put("key", HexEncoder.encode(regCode));
try {
prefs.flush();
} catch (BackingStoreException e) {
e.printStackTrace(System.err);
}
}
/**
* Clear away registration details for a game
* @param game
*/
public static void clearRegistration(String game) {
Preferences prefs = Preferences.userNodeForPackage(RegistrationDetails.class).node(game);
try {
prefs.removeNode();
prefs.flush();
} catch (BackingStoreException e) {
e.printStackTrace(System.err);
}
}
/**
* Check to ensure that this is a valid registration.
* @param a public encryption key to use as validation
* @return true if the registration is valid, false otherwise
* @throws Exception if validation could not be performed for some
* reason (in which case the code isn't valid anyway)
*/
public boolean validate(PublicKey publicKey) throws Exception {
if (regCode == null) {
return false;
}
// Try three different encodings
try {
if (validate(publicKey, ENCODING)) {
return true;
}
} catch (Exception e) {
e.printStackTrace(System.err);
}
try {
if (validate(publicKey, OLD_ENCODING)) {
return true;
}
} catch (Exception e) {
e.printStackTrace(System.err);
}
return validate(publicKey, null);
}
private boolean validate(PublicKey publicKey, String encoding) throws Exception {
Signature dsa = Signature.getInstance(SIGNATURE_ALGORITHM);
dsa.initVerify(publicKey);
if (encoding != null) {
dsa.update(name.getBytes(encoding));
dsa.update(address.getBytes(encoding));
dsa.update(email.getBytes(encoding));
} else {
dsa.update(name.getBytes());
dsa.update(address.getBytes());
dsa.update(email.getBytes());
}
return dsa.verify(regCode);
}
/**
* Create the regcode for these user details using a private key.
* @param privateKey The private key to use
* @throws Exception if something goes wrong
*/
public void register(PrivateKey privateKey) throws Exception {
Signature dsa = Signature.getInstance(SIGNATURE_ALGORITHM);
dsa.initSign(privateKey);
dsa.update(name.getBytes(ENCODING));
dsa.update(address.getBytes(ENCODING));
dsa.update(email.getBytes(ENCODING));
regCode = dsa.sign();
}
/**
* Deregister the game! This happens when a game finds itself using the
* hiscore server and the hiscore server reports them as banned.
* Nothing happens immediately but the regcode will be invalid in a few days
* days...
*/
public void deregister() {
try {
Preferences prefs = Preferences.userNodeForPackage(RegistrationDetails.class).node(game);
if (prefs.get(ENCRYPT_KEY, "").equals(name)) {
// First export registration over the top of the current one, so the one
// that's stored has a 21-day expiry...
toPreferences();
// Then clear the little flag that tells us to check the date by sneakily
// putting a space on the end
prefs.put(ENCRYPT_KEY, name+" ");
}
} catch (Exception e) {
}
}
/**
* @return the game
*/
public String getGame() {
return game;
}
/**
* @return the email
*/
public String getEmail() {
return email;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @return the address
*/
public String getAddress() {
return address;
}
/**
* @return the regcode
*/
public byte[] getRegCode() {
return regCode;
}
/**
* @param nameOnly TODO
* @return a String to display on the title screen
*/
public String toTitleScreen(boolean nameOnly) {
if (name.equals(UNREGISTERED_NAME)) {
return UNREGISTERED_NAME;
} else if (nameOnly) {
return "Registered to "+name;
} else {
return "Registered to "+name+" ("+email+") "+address;
}
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
if (name.equals(UNREGISTERED_NAME)) {
return "RegistrationDetails[UNREGISTERED]";
} else {
return "RegistrationDetails["+name+"/"+email+"/"+address+"]";
}
}
/**
* Check registration details. If we reckon the user is unregistered we'll throw
* an exception of some sort. Otherwise we'll return the registration details.
* @param gameTitle The game's title
* @return RegistrationDetails
* @throws Exception
*/
public static RegistrationDetails checkRegistration(String gameTitle) throws Exception {
RegistrationDetails testRegistrationDetails = new RegistrationDetails(gameTitle);
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(HexDecoder.decode(PUBLIC_KEY));
PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
if (testRegistrationDetails.validate(publicKey)) {
return testRegistrationDetails;
} else {
return null;
}
}
}