package games.strategy.engine.vault; import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.DESKeySpec; import games.strategy.debug.ClientLogger; import games.strategy.engine.message.IChannelMessenger; import games.strategy.engine.message.IChannelSubscribor; import games.strategy.engine.message.RemoteName; /** * A vault is a secure way for the client and server to share information without * trusting each other. * * <p> * Data can be locked in the vault by a node. This data then is not readable by other nodes until the data is unlocked. * </p> * * <p> * When the data is unlocked by the original node, other nodes can read the data. When data is put in the vault, it cant * be changed by the * originating node. * </p> * * <p> * NOTE: to allow the data locked in the vault to be gc'd, the <code>release(VaultID id)</code> method * should be called when it is no longer needed. * </p> */ public class Vault { private static final RemoteName VAULT_CHANNEL = new RemoteName("games.strategy.engine.vault.IServerVault.VAULT_CHANNEL", IRemoteVault.class); private static final String ALGORITHM = "DES"; private SecretKeyFactory mSecretKeyFactory; // 0xCAFEBABE // we encrypt both this value and data when we encrypt data. // when decrypting we ensure that KNOWN_VAL is correct // and thus guarantee that we are being given the right key private static final byte[] KNOWN_VAL = new byte[] {0xC, 0xA, 0xF, 0xE, 0xB, 0xA, 0xB, 0xE}; private final KeyGenerator m_keyGen; private final IChannelMessenger m_channelMessenger; // Maps VaultID -> SecretKey private final ConcurrentMap<VaultID, SecretKey> m_secretKeys = new ConcurrentHashMap<>(); // maps ValutID -> encrypted byte[] private final ConcurrentMap<VaultID, byte[]> m_unverifiedValues = new ConcurrentHashMap<>(); // maps VaultID -> byte[] private final ConcurrentMap<VaultID, byte[]> m_verifiedValues = new ConcurrentHashMap<>(); private final Object m_waitForLock = new Object(); /** * Creates a new instance of Vault. */ public Vault(final IChannelMessenger channelMessenger) { m_channelMessenger = channelMessenger; m_channelMessenger.registerChannelSubscriber(m_remoteVault, VAULT_CHANNEL); try { mSecretKeyFactory = SecretKeyFactory.getInstance(ALGORITHM); m_keyGen = KeyGenerator.getInstance(ALGORITHM); } catch (final NoSuchAlgorithmException e) { ClientLogger.logQuietly(e); throw new IllegalStateException("Nothing known about algorithm:" + ALGORITHM); } } public void shutDown() { m_channelMessenger.unregisterChannelSubscriber(m_remoteVault, VAULT_CHANNEL); } // serialize secret key as byte array to // preserve jdk 1.4 to 1.5 compatability // they should be compatable, but we are // getting errors with serializing secret keys private SecretKey bytesToKey(final byte[] bytes) { try { final DESKeySpec spec = new DESKeySpec(bytes); return mSecretKeyFactory.generateSecret(spec); } catch (final GeneralSecurityException e) { throw new IllegalStateException(e.getMessage()); } } private byte[] secretKeyToBytes(final SecretKey key) { DESKeySpec ks; try { ks = (DESKeySpec) mSecretKeyFactory.getKeySpec(key, DESKeySpec.class); return ks.getKey(); } catch (final GeneralSecurityException e) { throw new IllegalStateException(e.getMessage()); } } private IRemoteVault getRemoteBroadcaster() { return (IRemoteVault) m_channelMessenger.getChannelBroadcastor(VAULT_CHANNEL); } /** * place data in the vault. An encrypted form of the data is sent at this * time to all nodes. * * <p> * The same key used to encrypt the KNOWN_VALUE so that nodes can verify the key when it is used to decrypt the data. * </p> * * @param data * - the data to lock * @return the VaultId of the data */ public VaultID lock(final byte[] data) { final VaultID id = new VaultID(m_channelMessenger.getLocalNode()); final SecretKey key = m_keyGen.generateKey(); if (m_secretKeys.putIfAbsent(id, key) != null) { throw new IllegalStateException("dupliagte id:" + id); } // we already know it, so might as well keep it m_verifiedValues.put(id, data); Cipher cipher; try { cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, key); } catch (final NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException e) { ClientLogger.logQuietly(e); throw new IllegalStateException(e.getMessage()); } // join the data and known value into one array final byte[] dataAndCheck = joinDataAndKnown(data); byte[] encrypted; try { encrypted = cipher.doFinal(dataAndCheck); } catch (final Exception e) { ClientLogger.logQuietly(e); throw new IllegalStateException(e.getMessage()); } // tell the world getRemoteBroadcaster().addLockedValue(id, encrypted); return id; } /** * Join known and data into one array. * * <p> * package access so we can test. * </p> */ static byte[] joinDataAndKnown(final byte[] data) { final byte[] dataAndCheck = new byte[KNOWN_VAL.length + data.length]; System.arraycopy(KNOWN_VAL, 0, dataAndCheck, 0, KNOWN_VAL.length); System.arraycopy(data, 0, dataAndCheck, KNOWN_VAL.length, data.length); return dataAndCheck; } /** * allow other nodes to see the data. * * <p> * You can only unlock data that was locked by the same instance of the Vault * </p> * * @param id * - the vault id to unlock */ public void unlock(final VaultID id) { if (!id.getGeneratedOn().equals(m_channelMessenger.getLocalNode())) { throw new IllegalArgumentException("Cant unlock data that wasnt locked on this node"); } final SecretKey key = m_secretKeys.remove(id); // let everyone unlock it getRemoteBroadcaster().unlock(id, secretKeyToBytes(key)); } /** * Note - if an id has been released, then this will return false. * If this instance of vault locked id, then this method will return true * if the id has not been released. * * @return - has this id been unlocked */ public boolean isUnlocked(final VaultID id) { return m_verifiedValues.containsKey(id); } /** * Get the unlocked data. */ public byte[] get(final VaultID id) throws NotUnlockedException { if (m_verifiedValues.containsKey(id)) { return m_verifiedValues.get(id); } else if (m_unverifiedValues.containsKey(id)) { throw new NotUnlockedException(); } else { throw new IllegalStateException("Nothing known about id:" + id); } } /** * Do we know about the given vault id. */ public boolean knowsAbout(final VaultID id) { return m_verifiedValues.containsKey(id) || m_unverifiedValues.containsKey(id); } public List<VaultID> knownIds() { final ArrayList<VaultID> rVal = new ArrayList<>(m_verifiedValues.keySet()); rVal.addAll(m_unverifiedValues.keySet()); return rVal; } /** * Allow all data associated with the given vault id to be released and garbage collected * * <p> * An id can be released by any node. * </p> * * <p> * If the id has already been released, then nothing will happen. * </p> */ public void release(final VaultID id) { getRemoteBroadcaster().release(id); } private final IRemoteVault m_remoteVault = new IRemoteVault() { @Override public void addLockedValue(final VaultID id, final byte[] data) { if (id.getGeneratedOn().equals(m_channelMessenger.getLocalNode())) { return; } if (m_unverifiedValues.putIfAbsent(id, data) != null) { throw new IllegalStateException("duplicate values for id:" + id); } synchronized (m_waitForLock) { m_waitForLock.notifyAll(); } } @Override public void unlock(final VaultID id, final byte[] secretKeyBytes) { if (id.getGeneratedOn().equals(m_channelMessenger.getLocalNode())) { return; } final SecretKey key = bytesToKey(secretKeyBytes); Cipher cipher; try { cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, key); } catch (final NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException e) { ClientLogger.logQuietly(e); throw new IllegalStateException(e.getMessage()); } final byte[] encrypted = m_unverifiedValues.remove(id); byte[] decrypted; try { decrypted = cipher.doFinal(encrypted); } catch (final Exception e1) { e1.printStackTrace(); throw new IllegalStateException(e1.getMessage()); } if (decrypted.length < KNOWN_VAL.length) { throw new IllegalStateException("decrypted is not long enough to have known value, cheating is suspected"); } // check that the known value is correct // we use the known value to check that the key given to // us was the key used to encrypt the value in the first place for (int i = 0; i < KNOWN_VAL.length; i++) { if (KNOWN_VAL[i] != decrypted[i]) { throw new IllegalStateException("Known value of cipher not correct, cheating is suspected"); } } final byte[] data = new byte[decrypted.length - KNOWN_VAL.length]; System.arraycopy(decrypted, KNOWN_VAL.length, data, 0, data.length); if (m_verifiedValues.putIfAbsent(id, data) != null) { throw new IllegalStateException("duplicate values for id:" + id); } synchronized (m_waitForLock) { m_waitForLock.notifyAll(); } } @Override public void release(final VaultID id) { m_unverifiedValues.remove(id); m_verifiedValues.remove(id); } }; /** * Waits until we know about a given vault id. * waits for at most timeout milliseconds */ public void waitForID(final VaultID id, final long timeoutMS) { if (timeoutMS <= 0) { throw new IllegalArgumentException("Must suppply positive timeout argument"); } final long endTime = timeoutMS + System.currentTimeMillis(); while (System.currentTimeMillis() < endTime && !knowsAbout(id)) { synchronized (m_waitForLock) { if (knowsAbout(id)) { return; } try { final long waitTime = endTime - System.currentTimeMillis(); if (waitTime > 0) { m_waitForLock.wait(waitTime); } } catch (final InterruptedException e) { // not a big deal } } } } /** * Wait until the given id is unlocked. */ public void waitForIdToUnlock(final VaultID id, final long timeout) { if (timeout <= 0) { throw new IllegalArgumentException("Must suppply positive timeout argument"); } final long startTime = System.currentTimeMillis(); long leftToWait = timeout; while (leftToWait > 0 && !isUnlocked(id)) { synchronized (m_waitForLock) { if (isUnlocked(id)) { return; } try { m_waitForLock.wait(leftToWait); } catch (final InterruptedException e) { // not a big deal } leftToWait = startTime + timeout - System.currentTimeMillis(); } } } } interface IRemoteVault extends IChannelSubscribor { void addLockedValue(VaultID id, byte[] data); void unlock(VaultID id, byte[] secretKeyBytes); void release(VaultID id); }