From b247ae2ff83b0b41d004d9b1f13613e69c13f482 Mon Sep 17 00:00:00 2001 From: "Alejandro P. Revilla" Date: Mon, 18 Jun 2018 18:38:07 -0300 Subject: [PATCH] CryptoService now use a Supplier to get its password We added a simple 'SensitiveString' that encrypts/obfuscates the String using AES in order to minimize the chances of the clear unlock password gets swapped to disk. --- .../java/org/jpos/crypto/CryptoService.java | 30 +++-- .../java/org/jpos/crypto/SensitiveString.java | 105 ++++++++++++++++++ .../java/org/jpos/q2/cli/crypto/UNLOCK.java | 13 ++- .../org/jpos/crypto/SensitiveStringTest.java | 25 +++++ 4 files changed, 160 insertions(+), 13 deletions(-) create mode 100644 modules/cryptoservice/src/main/java/org/jpos/crypto/SensitiveString.java create mode 100644 modules/cryptoservice/src/test/java/org/jpos/crypto/SensitiveStringTest.java diff --git a/modules/cryptoservice/src/main/java/org/jpos/crypto/CryptoService.java b/modules/cryptoservice/src/main/java/org/jpos/crypto/CryptoService.java index a5eb7db8b2..5de20228b7 100644 --- a/modules/cryptoservice/src/main/java/org/jpos/crypto/CryptoService.java +++ b/modules/cryptoservice/src/main/java/org/jpos/crypto/CryptoService.java @@ -42,6 +42,7 @@ import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; /** * Provides AES encryption service @@ -78,7 +79,7 @@ public final class CryptoService extends QBeanSupport implements Runnable { private Random rnd = new SecureRandom(); private long ttl; private long duration; - private char[] unlock; + private Supplier unlock; /** * Encrypts data using the current key @@ -127,7 +128,7 @@ public byte[] aesDecrypt (UUID jobId, UUID keyId, byte[] encoded) throws Excepti UUID xid = xor(jobId, keyId); SecretKey sk = keys.rdp(xid); if (sk == null && unlock != null) { - loadKey(jobId, keyId, unlock); + loadKey(jobId, keyId, unlock.get().toCharArray()); sk = keys.rdp(xid); } if (sk == null) { @@ -168,16 +169,16 @@ public boolean unloadKey (UUID jobId, UUID keyId) { /** * Unlock the CryptoService */ - public boolean unlock (char[] password) { + public boolean unlock (Supplier passwordSupplier) { try { if (isLocked()) { // attempt encrypt/decrypt UUID id = UUID.randomUUID(); SecretKey sk = generateKey(); byte[] b = pgpEncrypt(id.toString(), sk.getEncoded()); - PGPHelper.decrypt(b, privKeyRing, password); + PGPHelper.decrypt(b, privKeyRing, passwordSupplier.get().toCharArray()); sem.acquire(); - this.unlock = password; + this.unlock = passwordSupplier; sem.release(); } return true; @@ -229,8 +230,13 @@ public void setConfiguration (Configuration cfg) throws ConfigurationException { ttl = cfg.getLong("ttl", 3600000L); duration = cfg.getLong("duration", 86400000L); String unlockPassword = cfg.get("unlock-password", null); - if (unlockPassword != null) - unlock (unlockPassword.toCharArray()); + if (unlockPassword != null) { + try { + unlock (new SensitiveString(unlockPassword)); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | NoSuchProviderException | InvalidAlgorithmParameterException e) { + throw new ConfigurationException(e); + } + } } private SecretKey generateKey() throws NoSuchAlgorithmException { @@ -269,8 +275,10 @@ private void registerKey(String k, String v) throws Exception { Logger.log(evt); } - private SecretKey getKey (UUID keyId, char[] password) throws Exception { - password = password != null ? password : unlock; + private SecretKey getKey (UUID keyId, char[] passPhrase) throws Exception { + if (passPhrase == null && unlock == null) + throw new SecurityException("Passphrase not available"); + passPhrase = passPhrase != null ? passPhrase : unlock.get().toCharArray(); String v = (String) DB.execWithTransaction(db -> { SysConfigManager mgr = new SysConfigManager(db, "key."); @@ -282,12 +290,12 @@ private SecretKey getKey (UUID keyId, char[] password) throws Exception { byte[] key = PGPHelper.decrypt( v.getBytes(), privKeyRing, - password != null ? password : unlock); + passPhrase + ); return new SecretKeySpec(key, 0, key.length, "AES"); } - private byte[] decrypt (SecretKey sk, IvParameterSpec iv, byte[] cryptogram) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, NoSuchProviderException, InvalidAlgorithmParameterException diff --git a/modules/cryptoservice/src/main/java/org/jpos/crypto/SensitiveString.java b/modules/cryptoservice/src/main/java/org/jpos/crypto/SensitiveString.java new file mode 100644 index 0000000000..b306729c94 --- /dev/null +++ b/modules/cryptoservice/src/main/java/org/jpos/crypto/SensitiveString.java @@ -0,0 +1,105 @@ +/* + * jPOS Project [http://jpos.org] + * Copyright (C) 2000-2018 jPOS Software SRL + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jpos.crypto; + +import org.jpos.iso.ISOUtil; +import org.jpos.security.SystemSeed; + +import javax.crypto.*; +import javax.crypto.spec.IvParameterSpec; +import java.nio.ByteBuffer; +import java.security.*; +import java.util.Arrays; +import java.util.Objects; +import java.util.Random; +import java.util.function.Supplier; + +public class SensitiveString implements Supplier { + private SecretKey key; + private byte[] encoded; + private static Random rnd; + private static final String AES = "AES/CBC/PKCS5Padding"; + + static { + try { + rnd = SecureRandom.getInstanceStrong(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e.getMessage()); + } + } + + + public SensitiveString(String s) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, NoSuchProviderException, InvalidAlgorithmParameterException { + key = generateKey(); + encoded = encrypt(s.getBytes()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SensitiveString that = (SensitiveString) o; + return this.get().equals(that.get()); + } + + private SecretKey generateKey() throws NoSuchAlgorithmException { + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + int maxKeyLength = Cipher.getMaxAllowedKeyLength(keyGen.getAlgorithm()); + keyGen.init(maxKeyLength == Integer.MAX_VALUE ? 256 : maxKeyLength); + return keyGen.generateKey(); + } + + private byte[] encrypt(byte[] b) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { + final Cipher cipher = Cipher.getInstance(AES); + final byte[] iv = randomIV(); + cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv)); + byte[] enc = cipher.doFinal(b); + ByteBuffer buf = ByteBuffer.allocate(iv.length + enc.length); + buf.put(ISOUtil.xor(iv, SystemSeed.getSeed(iv.length, iv.length))); + buf.put(enc); + return buf.array(); + } + private byte[] decrypt(byte[] encoded) + throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, + IllegalBlockSizeException, NoSuchProviderException, InvalidAlgorithmParameterException + { + byte[] iv = new byte[16]; + byte[] cryptogram = new byte[encoded.length - iv.length]; + System.arraycopy(encoded, 0, iv, 0, iv.length); + System.arraycopy(encoded, iv.length, cryptogram, 0, cryptogram.length); + final Cipher cipher = Cipher.getInstance(AES); + cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(ISOUtil.xor(iv, SystemSeed.getSeed(iv.length, iv.length)))); + return cipher.doFinal(cryptogram); + } + + private byte[] randomIV() { + final byte[] b = new byte[16]; + rnd.nextBytes(b); + return b; + } + + @Override + public String get() { + try { + return new String(decrypt(encoded)); + } catch (NoSuchPaddingException | NoSuchAlgorithmException | BadPaddingException | InvalidKeyException | NoSuchProviderException | IllegalBlockSizeException | InvalidAlgorithmParameterException e) { + throw new AssertionError(e.getMessage()); + } + } +} diff --git a/modules/cryptoservice/src/main/java/org/jpos/q2/cli/crypto/UNLOCK.java b/modules/cryptoservice/src/main/java/org/jpos/q2/cli/crypto/UNLOCK.java index d30bd9174c..98c6b47628 100644 --- a/modules/cryptoservice/src/main/java/org/jpos/q2/cli/crypto/UNLOCK.java +++ b/modules/cryptoservice/src/main/java/org/jpos/q2/cli/crypto/UNLOCK.java @@ -19,10 +19,19 @@ package org.jpos.q2.cli.crypto; import org.jpos.crypto.CryptoService; +import org.jpos.crypto.SensitiveString; import org.jpos.q2.CLICommand; import org.jpos.q2.CLIContext; import org.jpos.util.NameRegistrar; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; + @SuppressWarnings("unused") public class UNLOCK implements CLICommand { private CryptoService cs; @@ -50,7 +59,7 @@ private void usage (CLIContext cli) { cli.println ("Usage: UNLOCK"); } - private boolean unlock (CLIContext cli) { - return cs.unlock(cli.getReader().readLine("Password: ", '*').toCharArray()); + private boolean unlock (CLIContext cli) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, NoSuchProviderException, InvalidKeyException { + return cs.unlock(new SensitiveString(cli.getReader().readLine("Password: ", '*'))); } } diff --git a/modules/cryptoservice/src/test/java/org/jpos/crypto/SensitiveStringTest.java b/modules/cryptoservice/src/test/java/org/jpos/crypto/SensitiveStringTest.java new file mode 100644 index 0000000000..f2e6c347f7 --- /dev/null +++ b/modules/cryptoservice/src/test/java/org/jpos/crypto/SensitiveStringTest.java @@ -0,0 +1,25 @@ +package org.jpos.crypto; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class SensitiveStringTest { + @Test + public void testSS() throws Exception { + String s = this.toString(); + for (int i=0; i<15; i++) { + SensitiveString ss = new SensitiveString(s); + assertEquals("Should be equal", s, ss.get()); + s = s + System.lineSeparator() + s; + } + } + + @Test + public void testSSEquals() throws Exception { + String s = "The quick brown fox jumps over the lazy dog"; + SensitiveString ss0 = new SensitiveString(s); + SensitiveString ss1 = new SensitiveString(s); + assertEquals ("Equals should be true", ss0, ss1); + } +}