An optional secure channel can be created for authentication to ensure credentials are encrypted

when sent to the server.

To implement a secure authentication channel:

- Create a key pair using the com.threerings.presents.tool.KeyPairGen tool.  You will need to
distribute the public key with your client, and the private key with the server.

- On your server during initialization, use PresentsConnectionManager.setPrivateKey to set the
private key.  It can take the key string gerenated by KeyPairGen.  This will return true if the
key was sucessfully set and the server supports the encryption necessary.

- On your client before authenticating, use Client.setPublicKey to set the public key.  As with
setPrivateKey, it will return true if the client supports the encryption necessary.

That's it!  You should now be authenticating over a secure encrypted channel.  The server can 
still accept unsecured authentication attempts (for the purpose of telling the client it needs a
new version to get the server's public key).  A failure to decrypt the client credientials on the
server will return a new "m.failed_to_secure" authentication code.

Handshake process:
- The client generates a random 128-bit key, and encodes it with the public key using a 32-bit
salt (PublicKeyCredentials).  This is sent to the server as a SecureRequest.
- The server decrypts the i128-bit key using its private key and verifies it against the salt.  If
verification fails, a failed secure response is returned and the client will authenticate over a
clear channel.  If verification succeeds, the server generates a random 128-bit AES key and
encodes it with the 128-bit key sent from the client.  This is sent back to the client as a
SecureResponse.
- The client will decode the AES key sent from the server (using the random key it generated at
the start of the handshake).  Using the AES key, the client will encrypt their credentials using
an AESAuthRequest and send it to the server.
- The server can now decrypt the credentials from the client and pass the AESAuthRequest to the
configured authenticators to complete authentication.  If the server fails to decrypt the
credentials a "m.failed_to_secure" authentication code is returned to the client.



git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@6477 542714f4-19e9-0310-aa3c-eee0fc999fb1
This commit is contained in:
Mark Johnson
2011-02-04 19:38:53 +00:00
parent 540f84194d
commit ae6c134546
11 changed files with 886 additions and 8 deletions
@@ -25,6 +25,7 @@ import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.io.EOFException;
import java.io.IOException;
@@ -51,12 +52,16 @@ import com.threerings.io.ObjectOutputStream;
import com.threerings.io.UnreliableObjectInputStream;
import com.threerings.io.UnreliableObjectOutputStream;
import com.threerings.presents.net.AESAuthRequest;
import com.threerings.presents.net.AuthRequest;
import com.threerings.presents.net.AuthResponse;
import com.threerings.presents.net.AuthResponseData;
import com.threerings.presents.net.DownstreamMessage;
import com.threerings.presents.net.LogoffRequest;
import com.threerings.presents.net.PingRequest;
import com.threerings.presents.net.PublicKeyCredentials;
import com.threerings.presents.net.SecureRequest;
import com.threerings.presents.net.SecureResponse;
import com.threerings.presents.net.TransmitDatagramsRequest;
import com.threerings.presents.net.Transport;
import com.threerings.presents.net.UpstreamMessage;
@@ -616,13 +621,40 @@ public class BlockingCommunicator extends Communicator
// connect to the server
connect();
// construct an auth request and send it
sendMessage(new AuthRequest(_client.getCredentials(), _client.getVersion(),
_client.getBootGroups()));
// If a public key is specified, we'll attempt to establish a secure authentication
// channel
PublicKey key = _client.getPublicKey();
AuthResponse response = null;
if (key != null) {
PublicKeyCredentials pkcreds = new PublicKeyCredentials(key);
sendMessage(new SecureRequest(pkcreds, _client.getVersion()));
// now wait for the handshake
log.debug("Waiting for secure response.");
response = (AuthResponse)receiveMessage();
// If we've recieved a secure response, proceed with authentication
if (response instanceof SecureResponse) {
sendMessage(((SecureResponse)response).createAuthRequest(
pkcreds, _client.getCredentials(),
_client.getVersion(), _client.getBootGroups()));
// now wait for the auth response
log.debug("Waiting for auth response.");
response = (AuthResponse)receiveMessage();
}
} else {
// construct an auth request and send it
sendMessage(new AuthRequest(
_client.getCredentials(), _client.getVersion(), _client.getBootGroups()));
// now wait for the auth response
log.debug("Waiting for auth response.");
response = (AuthResponse)receiveMessage();
}
gotAuthResponse(response);
// now wait for the auth response
log.debug("Waiting for auth response.");
gotAuthResponse((AuthResponse)receiveMessage());
} catch (Exception e) {
log.debug("Logon failed: " + e);
@@ -23,6 +23,8 @@ package com.threerings.presents.client;
import java.util.HashSet;
import java.security.PublicKey;
import com.google.common.collect.Sets;
import com.samskivert.util.Interval;
@@ -45,6 +47,7 @@ import com.threerings.presents.net.Credentials;
import com.threerings.presents.net.PingRequest;
import com.threerings.presents.net.PongResponse;
import com.threerings.presents.net.ThrottleUpdatedMessage;
import com.threerings.presents.util.SecureUtil;
import static com.threerings.presents.Log.log;
@@ -198,6 +201,43 @@ public class Client
_creds = creds;
}
/**
* Returns the public key with which this client is currently configured to create a secure
* authentication channel to the server.
*/
public PublicKey getPublicKey ()
{
return _publicKey;
}
/**
* Sets the public key that will be used by this client to create a secure authentication
* channel with the server if the ciphers are supported. This should be done before any call
* to <code>logon</code>.
*
* @return true if the key is set
*/
public boolean setPublicKey (PublicKey key)
{
if (SecureUtil.ciphersSupported(key)) {
_publicKey = key;
return true;
}
return false;
}
/**
* Sets the public key that will be used by this client to create a secure authentication
* channel with the server if the ciphers are supported. This should be done before any call
* to <code>logon</code>.
*
* @return true if the key is set
*/
public boolean setPublicKey (String key)
{
return key == null ? false : setPublicKey(SecureUtil.stringToRSAPublicKey(key));
}
/**
* Returns the version string configured for this client.
*/
@@ -986,6 +1026,9 @@ public class Client
/** The data associated with our authentication response. */
protected AuthResponseData _authData;
/** Our public key. */
protected PublicKey _publicKey;
/** The unique id of our connection. */
protected int _connectionId = -1;
@@ -43,4 +43,7 @@ public interface AuthCodes
/** A code indicating that we failed to connect to the server on a port and
* are trying the next port in the list. */
public static final String TRYING_NEXT_PORT = "m.trying_next_port";
/** A code indicating that we failed to establish a secure connection. */
public static final String FAILED_TO_SECURE = "m.failed_to_secure";
}
@@ -0,0 +1,134 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2011 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.net;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import javax.crypto.Cipher;
import com.threerings.io.ObjectInputStream;
import com.threerings.io.ObjectOutputStream;
import com.threerings.presents.util.SecureUtil;
/**
* Sends an AES encrypted auth request to the server. It assumes that
* {@link SecureUtil#ciphersSupported} has succeeded.
*/
public class AESAuthRequest extends AuthRequest
{
/**
* Zero argument constructor used when unserializing an instance.
*/
public AESAuthRequest ()
{
super();
}
/**
* Constructs a auth request with the supplied credentials and client version information.
*/
public AESAuthRequest (byte[] key, Credentials creds, String version, String[] bootGroups)
{
super(null, version, bootGroups);
_clearCreds = creds;
_key = key;
}
@Override // documentation inherited
public Credentials getCredentials ()
{
return _clearCreds;
}
@Override // documentation inherited
public String toString ()
{
return "[type=AESAREQ, msgid=" + messageId + ", creds=" + _clearCreds +
", version=" + _version + "]";
}
/**
* Decrypts the request after transmission.
*/
public void decrypt (byte[] key)
throws IOException, ClassNotFoundException
{
if (_clearCreds != null) {
return;
}
_key = key;
try {
_contents = SecureUtil.getAESCipher(Cipher.DECRYPT_MODE, _key).doFinal(_contents);
} catch (GeneralSecurityException gse) {
throw new IOException("Failed to decrypt credentials", gse);
}
ByteArrayInputStream byteIn = new ByteArrayInputStream(_contents);
ObjectInputStream cipherIn = new ObjectInputStream(byteIn);
_clearCreds = (Credentials)cipherIn.readObject();
}
/**
* A customized AES encrypting write object.
*/
public void writeObject (ObjectOutputStream out)
throws IOException
{
out.defaultWriteObject();
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
ObjectOutputStream oOut = new ObjectOutputStream(byteOut);
oOut.writeObject(_clearCreds);
try {
byte[] encrypted =
SecureUtil.getAESCipher(Cipher.ENCRYPT_MODE, _key).doFinal(byteOut.toByteArray());
out.writeInt(encrypted.length);
out.write(encrypted);
} catch (GeneralSecurityException gse) {
throw new IOException("Failed to encrypt credentials", gse);
}
}
/**
* Read in our encrypted contents.
*/
public void readObject (ObjectInputStream in)
throws IOException, ClassNotFoundException
{
in.defaultReadObject();
_contents = new byte[in.readInt()];
in.read(_contents);
}
/** Our encryption key. */
protected transient byte[] _key;
/** Our encrypted contents. */
protected transient byte[] _contents;
/** Our unencrypted credentials. */
protected transient Credentials _clearCreds;
}
@@ -0,0 +1,86 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2011 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.net;
import java.security.PrivateKey;
import java.security.PublicKey;
import javax.crypto.Cipher;
import com.threerings.presents.util.SecureUtil;
/**
* Credentials based on a public key encrypted secret.
*/
public class PublicKeyCredentials extends Credentials
{
/**
* No-arg constructor.
*/
public PublicKeyCredentials ()
{
}
/**
* Create a public key credential.
*/
public PublicKeyCredentials (PublicKey key)
{
_secret = SecureUtil.createRandomKey(16);
_salt = SecureUtil.createRandomKey(4);
_encodedSecret = SecureUtil.encryptBytes(key, _secret, _salt);
}
/**
* Returns the secret.
*/
public byte[] getSecret ()
{
return _secret;
}
/**
* Decodes the secret.
*/
public byte[] getSecret (PrivateKey key)
{
if (_secret == null) {
_secret = SecureUtil.decryptBytes(key, _encodedSecret, _salt);
}
return _secret;
}
@Override // documentation inherited
public String getDatagramSecret ()
{
return new String(_encodedSecret);
}
/** Our transmitted key. */
protected byte[] _encodedSecret;
/** Our verification salt. */
protected byte[] _salt;
/** Our secret key. */
protected transient byte[] _secret;
}
@@ -0,0 +1,57 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2011 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.net;
import java.security.PrivateKey;
import java.security.PublicKey;
import javax.crypto.Cipher;
/**
* Used to create a secure channel to the server.
*/
public class SecureRequest extends AuthRequest
{
/**
* Zero argument constructor used when unserializing an instance.
*/
public SecureRequest ()
{
super();
}
/**
* Constructs a auth request with the supplied credentials and client version information.
*/
public SecureRequest (PublicKeyCredentials creds, String version)
{
super(creds, version, new String[0]);
}
/**
* Returns the secret from the credentials.
*/
public byte[] getSecret (PrivateKey key)
{
return ((PublicKeyCredentials)_creds).getSecret(key);
}
}
@@ -0,0 +1,84 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2011 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.net;
import java.security.PrivateKey;
import com.samskivert.util.StringUtil;
import com.threerings.presents.data.AuthCodes;
import com.threerings.presents.util.SecureUtil;
/**
* Used to indicate a authentication response based on a SecureRequest.
*/
public class SecureResponse extends AuthResponse
implements AuthCodes
{
/**
* Zero argument constructor used when unserializing an instance.
*/
public SecureResponse ()
{
super();
}
/**
* Creates a secure response with the response code.
*/
public SecureResponse (String code)
{
_data = new AuthResponseData();
_data.code = code;
}
/**
* Encodes the server secret in the response data, or sets the failed state.
*
* @return the server secret if successfully encoded, or null.
*/
public byte[] createSecret (PublicKeyCredentials pkcred, PrivateKey key, int length)
{
_data = new AuthResponseData();
byte[] clientSecret = pkcred.getSecret(key);
if (clientSecret == null) {
_data.code = FAILED_TO_SECURE;
return null;
}
byte[] secret = SecureUtil.createRandomKey(length);
_data.code = StringUtil.hexlate(SecureUtil.xorBytes(secret, clientSecret));
return secret;
}
/**
* Creates a request based on the secure response.
*/
public AuthRequest createAuthRequest (
PublicKeyCredentials pkcreds, Credentials creds, String version, String[] bootGroups)
{
if (_data.code.equals(FAILED_TO_SECURE)) {
return new AuthRequest(creds, version, bootGroups);
}
byte[] secret = SecureUtil.xorBytes(StringUtil.unhexlate(_data.code), pkcreds.getSecret());
return new AESAuthRequest(secret, creds, version, bootGroups);
}
}
@@ -21,11 +21,24 @@
package com.threerings.presents.server.net;
import java.io.IOException;
import java.security.PrivateKey;
import java.security.SecureRandom;
import com.samskivert.util.ObjectUtil;
import com.samskivert.util.StringUtil;
import com.threerings.util.Name;
import com.threerings.presents.data.AuthCodes;
import com.threerings.presents.net.AESAuthRequest;
import com.threerings.presents.net.AuthRequest;
import com.threerings.presents.net.AuthResponse;
import com.threerings.presents.net.DownstreamMessage;
import com.threerings.presents.net.Message;
import com.threerings.presents.net.PublicKeyCredentials;
import com.threerings.presents.net.SecureRequest;
import com.threerings.presents.net.SecureResponse;
import static com.threerings.presents.Log.log;
@@ -39,15 +52,60 @@ public class AuthingConnection extends PresentsConnection
{
setMessageHandler(new MessageHandler() {
public void handleMessage (Message msg) {
if (_serverSecret == null) {
// first see if the client is trying to start secure authentication
try {
SecureRequest secreq = (SecureRequest)msg;
PrivateKey key = _pcmgr.getPrivateKey();
// fail quickly if we don't support secure connections
if (key == null) {
safePostMessage(new SecureResponse(AuthCodes.FAILED_TO_SECURE));
} else {
// generate a server key and encode it using the client key
SecureResponse resp = new SecureResponse();
_serverSecret = resp.createSecret(
(PublicKeyCredentials)secreq.getCredentials(), key, 16);
safePostMessage(resp);
}
return;
} catch (ClassCastException cce) {
// Client didn't request a secure channel so proceed with normal
// authentication
}
} else {
try {
((AESAuthRequest)msg).decrypt(_serverSecret);
} catch (ClassCastException cce) {
log.warning("Received non-encrypted request during secure " +
"authentication process",
"conn", AuthingConnection.this, "msg", msg);
} catch (ClassNotFoundException cnfe) {
log.warning(
"Failed to decrypt request during secure authentication process",
"conn", AuthingConnection.this, "msg", msg, cnfe);
safePostMessage(new SecureResponse(AuthCodes.FAILED_TO_SECURE));
return;
} catch (IOException ioe) {
log.warning(
"Failed to decrypt request during secure authentication process",
"conn", AuthingConnection.this, "msg", msg, ioe);
safePostMessage(new SecureResponse(AuthCodes.FAILED_TO_SECURE));
return;
}
}
try {
// keep a handle on our auth request
_authreq = (AuthRequest)msg;
// post ourselves for processing by the authmgr
_pcmgr.authenticateConnection(AuthingConnection.this);
} catch (ClassCastException cce) {
log.warning("Received non-authreq message during authentication process",
"conn", AuthingConnection.this, "msg", msg);
}
if (_authreq != null) {
// post ourselves for processing by the authmgr
_pcmgr.authenticateConnection(AuthingConnection.this);
}
}
});
}
@@ -102,7 +160,21 @@ public class AuthingConnection extends PresentsConnection
return "[mode=AUTHING, addr=" + getInetAddress() + "]";
}
/**
* Callable from non-dobjmgr thread, this queues up a runnable on the dobjmgr thread to post
* the supplied message to this client.
*/
protected final void safePostMessage (final DownstreamMessage msg)
{
_pcmgr._omgr.postRunnable(new Runnable() {
public void run () {
postMessage(msg);
}
});
}
protected AuthRequest _authreq;
protected AuthResponse _authrsp;
protected Name _authname;
protected byte[] _serverSecret;
}
@@ -31,6 +31,8 @@ import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.security.PrivateKey;
import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -61,6 +63,7 @@ import com.threerings.presents.server.DummyAuthenticator;
import com.threerings.presents.server.PresentsDObjectMgr;
import com.threerings.presents.server.ReportManager;
import com.threerings.presents.util.DatagramSequencer;
import com.threerings.presents.util.SecureUtil;
import com.threerings.nio.conman.Connection;
import com.threerings.nio.conman.ConnectionManager;
@@ -143,6 +146,38 @@ public class PresentsConnectionManager extends ConnectionManager
_authors.add(author);
}
/**
* Sets the private key if the ciphers are supported.
*
* @return true if the key is set
*/
public boolean setPrivateKey (PrivateKey key)
{
if (SecureUtil.ciphersSupported(key)) {
_privateKey = key;
return true;
}
return false;
}
/**
* Sets the private key if the ciphers are supported.
*
* @return true if the key is set
*/
public boolean setPrivateKey (String key)
{
return key == null ? false : setPrivateKey(SecureUtil.stringToRSAPrivateKey(key));
}
/**
* Returns the private key used in secure authentication.
*/
public PrivateKey getPrivateKey ()
{
return _privateKey;
}
/**
* Called when a datagram message is ready to be read off its channel.
*/
@@ -522,6 +557,7 @@ public class PresentsConnectionManager extends ConnectionManager
* of authentication and then passes normal authentications through. */
@Inject(optional=true) protected Authenticator _author = new DummyAuthenticator();
protected List<ChainedAuthenticator> _authors = Lists.newArrayList();
protected PrivateKey _privateKey;
protected Queue<AuthingConnection> _authq = Queue.newQueue();
protected Queue<Tuple<Connection, InetSocketAddress>> _connectq = Queue.newQueue();
@@ -0,0 +1,51 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2011 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.tools;
import java.security.KeyPair;
import com.threerings.presents.util.SecureUtil;
/**
* Generates a RSA public/private key pair and outputs them in a format suitable for a properties
* file.
*/
public class KeyPairGen
{
public static void main (String[] args)
{
if (args.length != 1) {
System.err.println("Usage: KeyPairGen bits");
System.exit(-1);
}
try {
int bits = Integer.parseInt(args[0]);
KeyPair kp = SecureUtil.genRSAKeyPair(bits);
System.out.println("key.public = " + SecureUtil.RSAKeyToString(kp.getPublic()));
System.out.println("key.private = " + SecureUtil.RSAKeyToString(kp.getPrivate()));
} catch (NumberFormatException nfe) {
System.err.println("Usage: KeyPairGen bits");
System.exit(-1);
}
}
}
@@ -0,0 +1,280 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2011 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.util;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import static com.threerings.presents.Log.log;
/**
* Security utilities for performing secure authentication.
*/
public class SecureUtil
{
/**
* Creates our AES cipher.
*/
public static Cipher getAESCipher(int mode, byte[] key)
{
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec aesKey = new SecretKeySpec(key, "AES");
cipher.init(mode, aesKey, IVPS);
return cipher;
} catch (GeneralSecurityException gse) {
log.warning("Failed to create cipher", gse);
}
return null;
}
/**
* Creates our RSA cipher.
*/
public static Cipher getRSACipher(PrivateKey key)
{
return getRSACipher(Cipher.DECRYPT_MODE, key);
}
/**
* Creates our RSA cipher.
*/
public static Cipher getRSACipher(PublicKey key)
{
return getRSACipher(Cipher.ENCRYPT_MODE, key);
}
/**
* Creates our RSA cipher.
*/
public static Cipher getRSACipher(int mode, Key key)
{
try {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(mode, key);
return cipher;
} catch (GeneralSecurityException gse) {
log.warning("Failed to create cipher", gse);
}
return null;
}
/**
* Creates an RSA key pair.
*/
public static KeyPair genRSAKeyPair (int bits)
{
try {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(bits);
return kpg.genKeyPair();
} catch (GeneralSecurityException gse) {
log.warning("Failed to create key pair", gse);
}
return null;
}
/**
* Converts an key to a string suitable for a properties file.
*/
public static String RSAKeyToString (PublicKey key)
{
try {
KeyFactory kf = KeyFactory.getInstance("RSA");
RSAPublicKeySpec spec = kf.getKeySpec(key, RSAPublicKeySpec.class);
StringBuilder buf = new StringBuilder();
buf.append(spec.getModulus().toString(16))
.append(SPLIT)
.append(spec.getPublicExponent().toString(16));
return buf.toString();
} catch (GeneralSecurityException gse) {
log.warning("Failed to convert key to string", gse);
}
return null;
}
/**
* Converts an key to a string suitable for a properties file.
*/
public static String RSAKeyToString (PrivateKey key)
{
try {
KeyFactory kf = KeyFactory.getInstance("RSA");
RSAPrivateKeySpec spec = kf.getKeySpec(key, RSAPrivateKeySpec.class);
StringBuilder buf = new StringBuilder();
buf.append(spec.getModulus().toString(16))
.append(SPLIT)
.append(spec.getPrivateExponent().toString(16));
return buf.toString();
} catch (GeneralSecurityException gse) {
log.warning("Failed to convert key to string", gse);
}
return null;
}
/**
* Creates a public key from the supplied string.
*/
public static PublicKey stringToRSAPublicKey (String str)
{
try {
BigInteger mod = new BigInteger(str.substring(0, str.indexOf(SPLIT)), 16);
BigInteger exp = new BigInteger(str.substring(str.indexOf(SPLIT) + 1), 16);
RSAPublicKeySpec keySpec = new RSAPublicKeySpec(mod, exp);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePublic(keySpec);
} catch (NumberFormatException nfe) {
log.warning("Failed to read key from string.", "str", str, nfe);
} catch (GeneralSecurityException gse) {
log.warning("Failed to read key from string.", "str", str, gse);
}
return null;
}
/**
* Creates a private key from the supplied string.
*/
public static PrivateKey stringToRSAPrivateKey (String str)
{
try {
BigInteger mod = new BigInteger(str.substring(0, str.indexOf(SPLIT)), 16);
BigInteger exp = new BigInteger(str.substring(str.indexOf(SPLIT) + 1), 16);
RSAPrivateKeySpec keySpec = new RSAPrivateKeySpec(mod, exp);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(keySpec);
} catch (NumberFormatException nfe) {
log.warning("Failed to read key from string.", "str", str, nfe);
} catch (GeneralSecurityException gse) {
log.warning("Failed to read key from string.", "str", str, gse);
}
return null;
}
/**
* Returns true if we can generate our ciphers.
*/
public static boolean ciphersSupported (PrivateKey key)
{
return getRSACipher(key) != null && getAESCipher(Cipher.ENCRYPT_MODE, new byte[16]) != null;
}
/**
* Returns true if we can generate our ciphers.
*/
public static boolean ciphersSupported (PublicKey key)
{
return getRSACipher(key) != null && getAESCipher(Cipher.ENCRYPT_MODE, new byte[16]) != null;
}
/**
* Creates a random key.
*/
public static byte[] createRandomKey (int length)
{
byte[] secret = new byte[length];
_rand.nextBytes(secret);
return secret;
}
/**
* Encrypts a secret key and salt with a public key.
*/
public static byte[] encryptBytes (PublicKey key, byte[] secret, byte[] salt)
{
byte[] encrypt = new byte[secret.length + salt.length];
for (int ii = 0; ii < secret.length; ii++) {
encrypt[ii] = secret[ii];
}
for (int ii = 0; ii < salt.length; ii++) {
encrypt[secret.length + ii] = salt[ii];
}
try {
return getRSACipher(key).doFinal(encrypt);
} catch (GeneralSecurityException gse) {
log.warning("Failed to encrypt bytes", gse);
}
return encrypt;
}
/**
* Decrypts a secret key and checks for tailing salt.
*
* @return the secret key, or null on failure or non-matching salt.
*/
public static byte[] decryptBytes (PrivateKey key, byte[] encrypted, byte[] salt)
{
try {
byte[] decrypted = getRSACipher(key).doFinal(encrypted);
for (int ii = 0; ii < salt.length; ii++) {
if (decrypted[decrypted.length - salt.length + ii] != salt[ii]) {
return null;
}
}
byte[] secret = new byte[decrypted.length - salt.length];
for (int ii = 0; ii < secret.length; ii++) {
secret[ii] = decrypted[ii];
}
return secret;
} catch (GeneralSecurityException gse) {
log.warning("Failed to dencrypt bytes", gse);
}
return null;
}
/**
* XORs a byte array against a key.
*/
public static byte[] xorBytes (byte[] data, byte[] key)
{
byte[] xored = new byte[data.length];
for (int ii = 0; ii < data.length; ii++) {
xored[ii] = (byte)(data[ii] ^ key[ii % key.length]);
}
return xored;
}
protected static final SecureRandom _rand = new SecureRandom();
/** Our split character. */
protected static final char SPLIT = '#';
/** Our initialization vector. */
protected static final byte[] IV = new byte[] {
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08
};
protected static final IvParameterSpec IVPS = new IvParameterSpec(IV);
}