diff --git a/src/main/java/com/threerings/presents/client/BlockingCommunicator.java b/src/main/java/com/threerings/presents/client/BlockingCommunicator.java index d39d0ec66..1f2d6e920 100644 --- a/src/main/java/com/threerings/presents/client/BlockingCommunicator.java +++ b/src/main/java/com/threerings/presents/client/BlockingCommunicator.java @@ -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); diff --git a/src/main/java/com/threerings/presents/client/Client.java b/src/main/java/com/threerings/presents/client/Client.java index 7815ea3b0..ee68a5f0c 100644 --- a/src/main/java/com/threerings/presents/client/Client.java +++ b/src/main/java/com/threerings/presents/client/Client.java @@ -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 logon. + * + * @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 logon. + * + * @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; diff --git a/src/main/java/com/threerings/presents/data/AuthCodes.java b/src/main/java/com/threerings/presents/data/AuthCodes.java index 1d2a7bd29..246ed2db6 100644 --- a/src/main/java/com/threerings/presents/data/AuthCodes.java +++ b/src/main/java/com/threerings/presents/data/AuthCodes.java @@ -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"; } diff --git a/src/main/java/com/threerings/presents/net/AESAuthRequest.java b/src/main/java/com/threerings/presents/net/AESAuthRequest.java new file mode 100644 index 000000000..6e4030c03 --- /dev/null +++ b/src/main/java/com/threerings/presents/net/AESAuthRequest.java @@ -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; +} diff --git a/src/main/java/com/threerings/presents/net/PublicKeyCredentials.java b/src/main/java/com/threerings/presents/net/PublicKeyCredentials.java new file mode 100644 index 000000000..131f66d77 --- /dev/null +++ b/src/main/java/com/threerings/presents/net/PublicKeyCredentials.java @@ -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; +} diff --git a/src/main/java/com/threerings/presents/net/SecureRequest.java b/src/main/java/com/threerings/presents/net/SecureRequest.java new file mode 100644 index 000000000..586b0fef5 --- /dev/null +++ b/src/main/java/com/threerings/presents/net/SecureRequest.java @@ -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); + } +} diff --git a/src/main/java/com/threerings/presents/net/SecureResponse.java b/src/main/java/com/threerings/presents/net/SecureResponse.java new file mode 100644 index 000000000..2e0ddf8b0 --- /dev/null +++ b/src/main/java/com/threerings/presents/net/SecureResponse.java @@ -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); + } +} diff --git a/src/main/java/com/threerings/presents/server/net/AuthingConnection.java b/src/main/java/com/threerings/presents/server/net/AuthingConnection.java index 9b885a0dc..90d877406 100644 --- a/src/main/java/com/threerings/presents/server/net/AuthingConnection.java +++ b/src/main/java/com/threerings/presents/server/net/AuthingConnection.java @@ -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; } diff --git a/src/main/java/com/threerings/presents/server/net/PresentsConnectionManager.java b/src/main/java/com/threerings/presents/server/net/PresentsConnectionManager.java index b979454f3..f27f8fc0b 100644 --- a/src/main/java/com/threerings/presents/server/net/PresentsConnectionManager.java +++ b/src/main/java/com/threerings/presents/server/net/PresentsConnectionManager.java @@ -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 _authors = Lists.newArrayList(); + protected PrivateKey _privateKey; protected Queue _authq = Queue.newQueue(); protected Queue> _connectq = Queue.newQueue(); diff --git a/src/main/java/com/threerings/presents/tools/KeyPairGen.java b/src/main/java/com/threerings/presents/tools/KeyPairGen.java new file mode 100644 index 000000000..fa0e17072 --- /dev/null +++ b/src/main/java/com/threerings/presents/tools/KeyPairGen.java @@ -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); + } + } +} diff --git a/src/main/java/com/threerings/presents/util/SecureUtil.java b/src/main/java/com/threerings/presents/util/SecureUtil.java new file mode 100644 index 000000000..684de930b --- /dev/null +++ b/src/main/java/com/threerings/presents/util/SecureUtil.java @@ -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); +}