extensions/net.sf.basedb.otp/trunk/src/net/sf/basedb/otp/CryptUtil.java

Code
Comments
Other
Rev Date Author Line
4853 15 Jun 18 nicklas 1 package net.sf.basedb.otp;
4853 15 Jun 18 nicklas 2
4853 15 Jun 18 nicklas 3 import java.nio.charset.Charset;
4853 15 Jun 18 nicklas 4 import java.security.GeneralSecurityException;
4853 15 Jun 18 nicklas 5 import java.security.SecureRandom;
4853 15 Jun 18 nicklas 6 import java.util.Base64;
4853 15 Jun 18 nicklas 7 import java.util.Base64.Decoder;
4853 15 Jun 18 nicklas 8 import java.util.Base64.Encoder;
4853 15 Jun 18 nicklas 9 import java.util.Random;
4853 15 Jun 18 nicklas 10
4853 15 Jun 18 nicklas 11 import javax.crypto.Cipher;
4853 15 Jun 18 nicklas 12 import javax.crypto.SecretKey;
4853 15 Jun 18 nicklas 13 import javax.crypto.SecretKeyFactory;
4853 15 Jun 18 nicklas 14 import javax.crypto.spec.IvParameterSpec;
4853 15 Jun 18 nicklas 15 import javax.crypto.spec.PBEKeySpec;
4853 15 Jun 18 nicklas 16 import javax.crypto.spec.SecretKeySpec;
4853 15 Jun 18 nicklas 17
4855 18 Jun 18 nicklas 18 import net.sf.basedb.util.Values;
4853 15 Jun 18 nicklas 19
4855 18 Jun 18 nicklas 20
4853 15 Jun 18 nicklas 21 /**
4853 15 Jun 18 nicklas 22   Helper class for encrypting secret keys before they are stored in
4853 15 Jun 18 nicklas 23   the database and to get them back for OTP validation. 
4853 15 Jun 18 nicklas 24   
4853 15 Jun 18 nicklas 25   Encryption:
4853 15 Jun 18 nicklas 26   We use a master password that is located in base-otp.properties and a seed 
4853 15 Jun 18 nicklas 27   given by the internal ID of the user account. The PBKDF2 algorithm is used
4853 15 Jun 18 nicklas 28   to create a hash (SHA256) that becomes the 128 bit AES key. This key is used 
4853 15 Jun 18 nicklas 29   together with a random initialization vector to encrypt the OTP secret keys. 
4853 15 Jun 18 nicklas 30   The result is stored a Base64-encoded string in the database. The string consists 
4855 18 Jun 18 nicklas 31   of the number of iterations, the initialization vector and the encrypted text 
4855 18 Jun 18 nicklas 32   separated with a $.
4853 15 Jun 18 nicklas 33   
4853 15 Jun 18 nicklas 34   Decryption:
4853 15 Jun 18 nicklas 35   The text stored in the database is retrieved and split on the $ character. The
4855 18 Jun 18 nicklas 36   number of iterations and the initialization vector is easily re-created and 
4855 18 Jun 18 nicklas 37   once again we use the master password and the internal ID to generate the AES key. 
4855 18 Jun 18 nicklas 38   The key is used to decrypt the encrypted text from the database which gives us 
4855 18 Jun 18 nicklas 39   the OTP secret keys.
4853 15 Jun 18 nicklas 40
4853 15 Jun 18 nicklas 41   Important:
4853 15 Jun 18 nicklas 42   The master password cannot be changed without invalidating all encrypted data.
4853 15 Jun 18 nicklas 43   If the master password really need to be changed then the Users.otp_secretkey
4853 15 Jun 18 nicklas 44   column need to be nullified for all users. The users must then re-configure
4853 15 Jun 18 nicklas 45   their OTP setup before they can login again.
4853 15 Jun 18 nicklas 46   
4853 15 Jun 18 nicklas 47   The internal ID of the user accounts is used as a seed to generate a salt
4853 15 Jun 18 nicklas 48   value and also for the number of iterations in the key generation step. 
4853 15 Jun 18 nicklas 49   Encrypted data can't be copied from one user account to another since it
4853 15 Jun 18 nicklas 50   will not be possible to decrypt it.
4853 15 Jun 18 nicklas 51
4853 15 Jun 18 nicklas 52   @author nicklas
4853 15 Jun 18 nicklas 53   @since 1.0
4853 15 Jun 18 nicklas 54 */
4853 15 Jun 18 nicklas 55 public final class CryptUtil 
4853 15 Jun 18 nicklas 56 {
4853 15 Jun 18 nicklas 57
4853 15 Jun 18 nicklas 58   private static final int KEY_BITS = 128;
4853 15 Jun 18 nicklas 59   private static final int KEY_BYTES = KEY_BITS / 8;
4853 15 Jun 18 nicklas 60   
4853 15 Jun 18 nicklas 61   private CryptUtil()
4853 15 Jun 18 nicklas 62   {}
4853 15 Jun 18 nicklas 63   
4853 15 Jun 18 nicklas 64   /**
4853 15 Jun 18 nicklas 65     Encrypt the text. The result is returned as a single string with
4855 18 Jun 18 nicklas 66     the number of iterations, initialization vector (IV) and the encrypted 
4855 18 Jun 18 nicklas 67     text in Base64 encoding separated with a $.
4853 15 Jun 18 nicklas 68   */
4853 15 Jun 18 nicklas 69   public static final String encrypt(String text, long seed)
4853 15 Jun 18 nicklas 70     throws GeneralSecurityException
4853 15 Jun 18 nicklas 71   {
4853 15 Jun 18 nicklas 72     Encoder encoder = Base64.getEncoder();
4853 15 Jun 18 nicklas 73     byte[] plainText = text.getBytes(Charset.forName("UTF-8"));
4853 15 Jun 18 nicklas 74     
4853 15 Jun 18 nicklas 75     // Create the secret key to use for encrypting
4853 15 Jun 18 nicklas 76     // It will be deterministic for a given salt
4855 18 Jun 18 nicklas 77     int iterations = 10000+(int)(seed % 10000);
4855 18 Jun 18 nicklas 78     SecretKey key = secretKey(seed, iterations);
4853 15 Jun 18 nicklas 79     IvParameterSpec iv = randomIv();
4853 15 Jun 18 nicklas 80     
4853 15 Jun 18 nicklas 81     // AES with 128 bits should be enough
4853 15 Jun 18 nicklas 82     // A randomly generated IV seems to be the general recommendation
4853 15 Jun 18 nicklas 83     Cipher aes = Cipher.getInstance("AES/CBC/PKCS5Padding");
4853 15 Jun 18 nicklas 84     aes.init(Cipher.ENCRYPT_MODE, key, iv);
4853 15 Jun 18 nicklas 85   
4853 15 Jun 18 nicklas 86     // Encrypt it!
4853 15 Jun 18 nicklas 87     byte[] encrypted = aes.doFinal(plainText);
4853 15 Jun 18 nicklas 88
4855 18 Jun 18 nicklas 89     // Final result should be <iterations>$<IV>$<encrypted> encoded with Base64
4855 18 Jun 18 nicklas 90     return iterations + "$" + encoder.encodeToString(aes.getIV()) + "$" + encoder.encodeToString(encrypted);
4853 15 Jun 18 nicklas 91   }
4853 15 Jun 18 nicklas 92   
4853 15 Jun 18 nicklas 93   /**
4853 15 Jun 18 nicklas 94     Decrypt the string. It should be a single string that contains the
4855 18 Jun 18 nicklas 95     number of iterations, the initialization vector (IV) and the encrypted 
4855 18 Jun 18 nicklas 96     text in Base64 format separated with a $.
4853 15 Jun 18 nicklas 97   */
4853 15 Jun 18 nicklas 98   public static final String decrypt(String cyptedText, long seed)
4853 15 Jun 18 nicklas 99     throws GeneralSecurityException
4853 15 Jun 18 nicklas 100   {
4853 15 Jun 18 nicklas 101     Decoder decoder = Base64.getDecoder();
4853 15 Jun 18 nicklas 102     
4855 18 Jun 18 nicklas 103     String[] c = cyptedText.split("\\$", 3);
4855 18 Jun 18 nicklas 104     int iterations = Values.getInt(c[0]);
4855 18 Jun 18 nicklas 105     IvParameterSpec iv = new IvParameterSpec(decoder.decode(c[1]));
4855 18 Jun 18 nicklas 106     byte[] encrypted = decoder.decode(c[2]);
4853 15 Jun 18 nicklas 107     
4853 15 Jun 18 nicklas 108     // Create the secret key to use for decrypting
4853 15 Jun 18 nicklas 109     // It will be deterministic for a given salt
4855 18 Jun 18 nicklas 110     SecretKey key = secretKey(seed, iterations);
4853 15 Jun 18 nicklas 111     
4853 15 Jun 18 nicklas 112     Cipher aes = Cipher.getInstance("AES/CBC/PKCS5Padding");
4853 15 Jun 18 nicklas 113     aes.init(Cipher.DECRYPT_MODE, key, iv);
4853 15 Jun 18 nicklas 114     
4853 15 Jun 18 nicklas 115     byte[] plainText = aes.doFinal(encrypted);
4853 15 Jun 18 nicklas 116     return new String(plainText, Charset.forName("UTF-8"));
4853 15 Jun 18 nicklas 117   }
4853 15 Jun 18 nicklas 118   
4853 15 Jun 18 nicklas 119   /**
4853 15 Jun 18 nicklas 120     Generate an encryption key for a given seed. This method uses
4853 15 Jun 18 nicklas 121     the master password and the seed to generate an AES encryption
4853 15 Jun 18 nicklas 122     key. For a given password and seed it should generate the same
4853 15 Jun 18 nicklas 123     encryption key every time. If the seed is the ID of the user
4853 15 Jun 18 nicklas 124     entry in the database we will get a key that is unique for
4853 15 Jun 18 nicklas 125     each user and it will not be possible to copy-and-paste the encrypted
4853 15 Jun 18 nicklas 126     data between user accounts.
4853 15 Jun 18 nicklas 127   */
4855 18 Jun 18 nicklas 128   private static final SecretKey secretKey(long seed, int iterations)
4853 15 Jun 18 nicklas 129     throws GeneralSecurityException
4853 15 Jun 18 nicklas 130   {
4853 15 Jun 18 nicklas 131     byte[] salt = saltFromSeed(seed);
4853 15 Jun 18 nicklas 132     
4853 15 Jun 18 nicklas 133     String password = masterPassword();
4853 15 Jun 18 nicklas 134   
4853 15 Jun 18 nicklas 135     // Generate a hash of the password using the salt we get from the seed
4853 15 Jun 18 nicklas 136     // DO NOT CHANGE THE PARAMETERS HERE SINCE IT WILL CAUSE EXISTING KEYS
4853 15 Jun 18 nicklas 137     // TO NOT BE USABLE
4855 18 Jun 18 nicklas 138     PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, KEY_BITS);
4853 15 Jun 18 nicklas 139     SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
4853 15 Jun 18 nicklas 140
4853 15 Jun 18 nicklas 141     return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
4853 15 Jun 18 nicklas 142   }
4853 15 Jun 18 nicklas 143   
4853 15 Jun 18 nicklas 144   /**
4853 15 Jun 18 nicklas 145     Get the master password from the configuration file.
4853 15 Jun 18 nicklas 146   */
4853 15 Jun 18 nicklas 147   private static final String masterPassword()
4853 15 Jun 18 nicklas 148   {
5183 06 Dec 18 nicklas 149     return Otp.getConfig(false).getProperty("master-password");
4853 15 Jun 18 nicklas 150   }
4853 15 Jun 18 nicklas 151   
4853 15 Jun 18 nicklas 152   /**
4853 15 Jun 18 nicklas 153     Generates a random initialization vector.
4853 15 Jun 18 nicklas 154   */
4853 15 Jun 18 nicklas 155   private static final IvParameterSpec randomIv()
4853 15 Jun 18 nicklas 156   {
4853 15 Jun 18 nicklas 157     byte[] iv = new byte[KEY_BYTES];
4853 15 Jun 18 nicklas 158     SecureRandom secureRandom = new SecureRandom();
4853 15 Jun 18 nicklas 159     secureRandom.nextBytes(iv);
4853 15 Jun 18 nicklas 160     return new IvParameterSpec(iv);
4853 15 Jun 18 nicklas 161   }
4853 15 Jun 18 nicklas 162   
4853 15 Jun 18 nicklas 163   /**
4853 15 Jun 18 nicklas 164     Generate a salt byte[] from the given seed. The same
4853 15 Jun 18 nicklas 165     seed value should create the same salt every time.
4853 15 Jun 18 nicklas 166     DO NOT CHANGE HOW THIS METHOD WORKS SINCE IT WILL CAUSE 
4853 15 Jun 18 nicklas 167     EXISTING KEYS TO NOT BE USABLE
4853 15 Jun 18 nicklas 168   */
4853 15 Jun 18 nicklas 169   private static final byte[] saltFromSeed(long seed)
4853 15 Jun 18 nicklas 170   {
4853 15 Jun 18 nicklas 171     byte[] salt = new byte[KEY_BYTES];
4853 15 Jun 18 nicklas 172     Random random = new Random(seed); 
4853 15 Jun 18 nicklas 173     random.nextBytes(salt);
4853 15 Jun 18 nicklas 174     return salt;
4853 15 Jun 18 nicklas 175   }
4853 15 Jun 18 nicklas 176 }