This class is responsible for storing an API Key and Secret pair. The secret is encrypted with AES in CFB mode, using a key derived from a passphrase, a random salt and a number of rounds of SHA1.
An additional hash of the plaintext and the salt is stored to allow for detecting incorrect passphrase entry.
This is tested and working, but I would love feedback on correctness or improvements, especially areas where the code can be simplified or replaced with built-in functionality I am unaware of.
The key id is a string UUID format, and the secret is provided as a base-64 encoded string.
package com.bitgrind.mtgox;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Properties;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import com.google.common.base.Charsets;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import com.google.common.io.BaseEncoding;
import com.google.common.io.Closer;
import com.google.common.primitives.Bytes;
public class ApiKey {
private static final String PROP_HASH = "hash";
private static final String PROP_SALT = "salt";
private static final String PROP_SECRET = "secret";
private static final String PROP_KEY = "key";
private static final BaseEncoding BASE64 = BaseEncoding.base64();
private static final BaseEncoding HEX = BaseEncoding.base16().lowerCase();
private static final byte[] IV = new byte[] {
0x10, 0x10, 0x01, 0x04, 0x01, 0x01, 0x01, 0x02,
0x14, 0x1c, 0x11, 0x44, 0x21, 0x4a, 0x22, 0x31
};
private static final int SALT_LENGTH = 16;
private static final int HASH_ROUNDS = 65535;
private static final String CIPHER_PARAMS = "AES/OFB8/NOPADDING";
private static final SecureRandom RND;
static {
try {
RND = SecureRandom.getInstance("SHA1PRNG");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private static byte[] createRandomSalt() {
byte[] salt = new byte[SALT_LENGTH];
RND.nextBytes(salt);
return salt;
}
private static SecretKey deriveKey(byte[] salt, int iterations, String passphrase) {
final HashFunction sha1 = Hashing.sha1();
byte[] hash = new byte[salt.length + (sha1.bits() / 8)];
HashCode code = sha1.hashBytes(Bytes.concat(salt, passphrase.getBytes(Charsets.UTF_8)));
int len = code.writeBytesTo(hash, 0, hash.length);
System.arraycopy(salt, 0, hash, len, salt.length);
for (int i=0; i < iterations; i++) {
code = sha1.hashBytes(hash);
len = code.writeBytesTo(hash, 0, hash.length);
System.arraycopy(salt, 0, hash, len, salt.length);
}
return new SecretKeySpec(code.asBytes(), 0, 16, "AES");
}
public static final ApiKeyResult read(InputStream in, String passphrase) {
Properties p = new Properties();
try {
p.load(in);
String apiKey = p.getProperty(PROP_KEY, "");
byte[] encryptedKey = BASE64.decode(p.getProperty(PROP_SECRET, "0"));
byte[] salt = HEX.decode(p.getProperty(PROP_SALT, "0"));
byte[] hash = HEX.decode(p.getProperty(PROP_HASH, "0"));
SecretKey key = deriveKey(salt, HASH_ROUNDS, passphrase);
Cipher aes = Cipher.getInstance(CIPHER_PARAMS);
aes.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(IV));
byte[] decryptedKey = aes.doFinal(encryptedKey);
byte[] computedHash = Hashing.sha512().newHasher().putBytes(salt).putBytes(decryptedKey).hash().asBytes();
if (!Arrays.equals(hash, computedHash)) {
return new ApiKeyResult("Wrong passphrase");
}
return new ApiKeyResult(new ApiKey(apiKey, BASE64.encode(decryptedKey)));
} catch (Exception e) {
return new ApiKeyResult(e);
}
}
private final String key;
private final String secret;
ApiKey(String key, String secret) {
this.key = key;
this.secret = secret;
}
public byte[] getKeyBytes() {
return HEX.decode(key.replace("-", ""));
}
public byte[] signMessage(byte[] content) throws InvalidKeyException {
Mac mac;
try {
mac = Mac.getInstance("HmacSHA512");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
SecretKeySpec secretKey = new SecretKeySpec(BASE64.decode(secret), "HmacSHA512");
mac.init(secretKey);
return mac.doFinal(content);
}
public final ApiKeyResult write(String passphrase, OutputStream out) {
Properties p = new Properties();
try {
byte[] salt = createRandomSalt();
SecretKey aesKey = deriveKey(salt, HASH_ROUNDS, passphrase);
Cipher aes = Cipher.getInstance(CIPHER_PARAMS);
aes.init(Cipher.ENCRYPT_MODE, aesKey, new IvParameterSpec(IV));
byte[] decryptedBytes = BASE64.decode(secret);
byte[] encryptedKey = aes.doFinal(decryptedBytes);
p.put(PROP_KEY, key);
p.put(PROP_SECRET, BASE64.encode(encryptedKey));
p.put(PROP_SALT, HEX.encode(salt));
p.put(PROP_HASH, Hashing.sha512().newHasher().putBytes(salt).putBytes(decryptedBytes).hash().toString());
p.store(out, null);
out.flush();
return new ApiKeyResult();
} catch (Exception e) {
return new ApiKeyResult(e);
}
}
}