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 |
Helper class for encrypting secret keys before they are stored in |
4853 |
15 Jun 18 |
nicklas |
the database and to get them back for OTP validation. |
4853 |
15 Jun 18 |
nicklas |
24 |
|
4853 |
15 Jun 18 |
nicklas |
Encryption: |
4853 |
15 Jun 18 |
nicklas |
We use a master password that is located in base-otp.properties and a seed |
4853 |
15 Jun 18 |
nicklas |
given by the internal ID of the user account. The PBKDF2 algorithm is used |
4853 |
15 Jun 18 |
nicklas |
to create a hash (SHA256) that becomes the 128 bit AES key. This key is used |
4853 |
15 Jun 18 |
nicklas |
together with a random initialization vector to encrypt the OTP secret keys. |
4853 |
15 Jun 18 |
nicklas |
The result is stored a Base64-encoded string in the database. The string consists |
4855 |
18 Jun 18 |
nicklas |
of the number of iterations, the initialization vector and the encrypted text |
4855 |
18 Jun 18 |
nicklas |
separated with a $. |
4853 |
15 Jun 18 |
nicklas |
33 |
|
4853 |
15 Jun 18 |
nicklas |
Decryption: |
4853 |
15 Jun 18 |
nicklas |
The text stored in the database is retrieved and split on the $ character. The |
4855 |
18 Jun 18 |
nicklas |
number of iterations and the initialization vector is easily re-created and |
4855 |
18 Jun 18 |
nicklas |
once again we use the master password and the internal ID to generate the AES key. |
4855 |
18 Jun 18 |
nicklas |
The key is used to decrypt the encrypted text from the database which gives us |
4855 |
18 Jun 18 |
nicklas |
the OTP secret keys. |
4853 |
15 Jun 18 |
nicklas |
40 |
|
4853 |
15 Jun 18 |
nicklas |
Important: |
4853 |
15 Jun 18 |
nicklas |
The master password cannot be changed without invalidating all encrypted data. |
4853 |
15 Jun 18 |
nicklas |
If the master password really need to be changed then the Users.otp_secretkey |
4853 |
15 Jun 18 |
nicklas |
column need to be nullified for all users. The users must then re-configure |
4853 |
15 Jun 18 |
nicklas |
their OTP setup before they can login again. |
4853 |
15 Jun 18 |
nicklas |
46 |
|
4853 |
15 Jun 18 |
nicklas |
The internal ID of the user accounts is used as a seed to generate a salt |
4853 |
15 Jun 18 |
nicklas |
value and also for the number of iterations in the key generation step. |
4853 |
15 Jun 18 |
nicklas |
Encrypted data can't be copied from one user account to another since it |
4853 |
15 Jun 18 |
nicklas |
will not be possible to decrypt it. |
4853 |
15 Jun 18 |
nicklas |
51 |
|
4853 |
15 Jun 18 |
nicklas |
@author nicklas |
4853 |
15 Jun 18 |
nicklas |
@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 |
Encrypt the text. The result is returned as a single string with |
4855 |
18 Jun 18 |
nicklas |
the number of iterations, initialization vector (IV) and the encrypted |
4855 |
18 Jun 18 |
nicklas |
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 |
// Create the secret key to use for encrypting |
4853 |
15 Jun 18 |
nicklas |
// 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 |
// AES with 128 bits should be enough |
4853 |
15 Jun 18 |
nicklas |
// 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 |
// Encrypt it! |
4853 |
15 Jun 18 |
nicklas |
87 |
byte[] encrypted = aes.doFinal(plainText); |
4853 |
15 Jun 18 |
nicklas |
88 |
|
4855 |
18 Jun 18 |
nicklas |
// 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 |
Decrypt the string. It should be a single string that contains the |
4855 |
18 Jun 18 |
nicklas |
number of iterations, the initialization vector (IV) and the encrypted |
4855 |
18 Jun 18 |
nicklas |
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 |
// Create the secret key to use for decrypting |
4853 |
15 Jun 18 |
nicklas |
// 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 |
Generate an encryption key for a given seed. This method uses |
4853 |
15 Jun 18 |
nicklas |
the master password and the seed to generate an AES encryption |
4853 |
15 Jun 18 |
nicklas |
key. For a given password and seed it should generate the same |
4853 |
15 Jun 18 |
nicklas |
encryption key every time. If the seed is the ID of the user |
4853 |
15 Jun 18 |
nicklas |
entry in the database we will get a key that is unique for |
4853 |
15 Jun 18 |
nicklas |
each user and it will not be possible to copy-and-paste the encrypted |
4853 |
15 Jun 18 |
nicklas |
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 |
// Generate a hash of the password using the salt we get from the seed |
4853 |
15 Jun 18 |
nicklas |
// DO NOT CHANGE THE PARAMETERS HERE SINCE IT WILL CAUSE EXISTING KEYS |
4853 |
15 Jun 18 |
nicklas |
// 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 |
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 |
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 |
Generate a salt byte[] from the given seed. The same |
4853 |
15 Jun 18 |
nicklas |
seed value should create the same salt every time. |
4853 |
15 Jun 18 |
nicklas |
DO NOT CHANGE HOW THIS METHOD WORKS SINCE IT WILL CAUSE |
4853 |
15 Jun 18 |
nicklas |
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 |
} |