/******************************************************************************* * Copyright (c) 2013 Lectorius, Inc. * Authors: * Vijay Pandurangan (vijayp@mitro.co) * Evan Jones (ej@mitro.co) * Adam Hilss (ahilss@mitro.co) * * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * You can contact the authors at inbound@mitro.co. *******************************************************************************/ package co.mitro.keyczar; import java.io.UnsupportedEncodingException; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; import org.keyczar.enums.CipherMode; import org.keyczar.exceptions.Base64DecodingException; import org.keyczar.exceptions.KeyczarException; import org.keyczar.interfaces.KeyczarReader; import org.keyczar.util.Base64Coder; import com.google.gson.Gson; public class KeyczarPBEReader implements KeyczarReader { private final KeyczarReader reader; private final String passphrase; // PBKDF2 NIST approved standard key derivation function private static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1"; // PBKDF2 RFC 2898 recommends at least 8 bytes (64 bits) of salt // http://tools.ietf.org/html/rfc2898#section-4 // but NIST recommends at least 16 bytes (128 bits; see Section 5.1) // http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf static final int SALT_BYTES = 16; // NIST suggests count to be 1000 as a minimum, but that seems poor. // 4 GPUs can do 3M attempts/second with 1000 iterations. See: // http://blog.agilebits.com/2013/04/16/1password-hashcat-strong-master-passwords/ // 10000 iterations; 7 mixed case random letters = 9 days to crack // 10000 iterations; 9 mixed case random letters = 193 years days to crack // C++ Keyczar uses 4096 iterations by default (crypto_factory.cc) @SuppressWarnings("unused") private static final int MIN_ITERATION_COUNT = 10000; // We use 50000 iterations by default to increase brute force difficulty // 7 mixed case random letters = 41 days to crack // 9 mixed case random letters = 867 years to crack static final int DEFAULT_ITERATION_COUNT = 50000; // PBKDF2 key derivation function public static byte[] pbkdf2(PBEKeySpec keySpec) { try { SecretKeyFactory factory = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM); SecretKey key = factory.generateSecret(keySpec); return key.getEncoded(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("Unexpected: unsupported key derivation function?", e); } catch (InvalidKeySpecException e) { throw new IllegalArgumentException("Invalid keySpec", e); } } private static final String AES_ALGORITHM = "AES"; /** * Reads encrypted key files from the given reader and decrypts them * with the given crypter. * * @param reader The reader to read files from. * @param crypter The crypter to decrypt keys with. */ public KeyczarPBEReader(KeyczarReader reader, String passphrase) { this.reader = reader; this.passphrase = passphrase; } String decryptKey(String encryptedKeyData) throws KeyczarException { // Parse the metadata PBEKeyczarKey pbeMetadata = parsePBEMetadata(encryptedKeyData); // generate the key PBEKeySpec spec = keySpecFromJson(pbeMetadata, passphrase); byte[] keyBytes = pbkdf2(spec); SecretKeySpec key = new SecretKeySpec(keyBytes, AES_ALGORITHM); // Decrypt the data IvParameterSpec iv = new IvParameterSpec(Base64Coder.decodeWebSafe(pbeMetadata.iv)); try { Cipher decryptingCipher = Cipher.getInstance(CipherMode.CBC.getMode()); decryptingCipher.init(Cipher.DECRYPT_MODE, key, iv); byte[] encryptedKey = Base64Coder.decodeWebSafe(pbeMetadata.key); byte[] decrypted = decryptingCipher.doFinal(encryptedKey); return new String(decrypted, "UTF-8"); } catch (java.security.GeneralSecurityException e) { throw new KeyczarException("Error decrypting PBE key", e); } catch (UnsupportedEncodingException e) { throw new RuntimeException("Should never occur"); } } @Override public String getKey() throws KeyczarException { return decryptKey(reader.getKey()); } @Override public String getKey(int version) throws KeyczarException { return decryptKey((reader.getKey(version))); } @Override public String getMetadata() throws KeyczarException { String originalMetadata = reader.getMetadata(); // GenericKeyczar throws an exception if it sees encrypted: true // hack around this return originalMetadata.replaceFirst("\"encrypted\":\\s*true", "\"encrypted\":false"); } static final class PBEKeyczarKey { public String cipher; public String hmac; public int iterationCount; public String iv; public String key; public String salt; } static String encryptKey(String key, String password) throws KeyczarException { PBEKeyczarKey pbeKey = new PBEKeyczarKey(); pbeKey.cipher = PBE_CIPHER; pbeKey.hmac = PBE_HMAC; pbeKey.iterationCount = DEFAULT_ITERATION_COUNT; // TODO: Figure out how to remove this base64 encoding round trip? byte[] salt = new byte[SALT_BYTES]; org.keyczar.util.Util.rand(salt); pbeKey.salt = Base64Coder.encodeWebSafe(salt); PBEKeySpec spec = keySpecFromJson(pbeKey, password); byte[] derivedKeyBytes = pbkdf2(spec); SecretKeySpec derivedKey = new SecretKeySpec(derivedKeyBytes, AES_ALGORITHM); // Decrypt the data byte[] ivBytes = new byte[PBE_AES_KEY_BYTES]; org.keyczar.util.Util.rand(ivBytes); pbeKey.iv = Base64Coder.encodeWebSafe(ivBytes); IvParameterSpec iv = new IvParameterSpec(ivBytes); try { Cipher encryptingCipher = Cipher.getInstance(CipherMode.CBC.getMode()); encryptingCipher.init(Cipher.ENCRYPT_MODE, derivedKey, iv); byte[] keyBytes = key.getBytes("UTF-8"); byte[] encrypted = encryptingCipher.doFinal(keyBytes); pbeKey.key = Base64Coder.encodeWebSafe(encrypted); } catch (java.security.GeneralSecurityException e) { throw new KeyczarException("Error encrypting key", e); } catch (UnsupportedEncodingException e) { throw new RuntimeException("Should never occur"); } Gson gson = new Gson(); return gson.toJson(pbeKey); } static PBEKeySpec keySpecFromJson(PBEKeyczarKey pbeKey, String password) { try { byte[] saltBytes = Base64Coder.decodeWebSafe(pbeKey.salt); char[] passwordChars = password.toCharArray(); // keyLength is in bits return new PBEKeySpec(passwordChars, saltBytes, pbeKey.iterationCount, PBE_AES_KEY_BYTES*8); } catch (Base64DecodingException e) { throw new RuntimeException(e); } } private static final String PBE_CIPHER = "AES128"; // TODO: Look this up from the cipher type static final int PBE_AES_KEY_BYTES = 16; private static final String PBE_HMAC = "HMAC_SHA1"; static PBEKeyczarKey parsePBEMetadata(String json) { Gson gson = new Gson(); PBEKeyczarKey pbeKey = gson.fromJson(json, PBEKeyczarKey.class); assert pbeKey.cipher.equals(PBE_CIPHER); assert pbeKey.hmac.equals(PBE_HMAC); assert pbeKey.iterationCount > 0; return pbeKey; } }