package freenet.node; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.util.Arrays; import java.util.Random; import freenet.crypt.BlockCipher; import freenet.crypt.MasterSecret; import freenet.crypt.PCFBMode; import freenet.crypt.RandomSource; import freenet.crypt.SHA256; import freenet.crypt.UnsupportedCipherException; import freenet.crypt.ciphers.Rijndael; import freenet.support.Fields; import freenet.support.io.Closer; import freenet.support.io.FileUtil; /** Keys read from the master keys file */ public class MasterKeys { // Currently we only encrypt the client cache final byte[] clientCacheMasterKey; private final byte[] databaseKey; private final byte[] tempfilesMasterSecret; final long flags; final static long FLAG_ENCRYPT_DATABASE = 2; public MasterKeys(byte[] clientCacheKey, byte[] databaseKey, byte[] tempfilesMasterSecret, long flags) { this.clientCacheMasterKey = clientCacheKey; this.databaseKey = databaseKey; this.flags = flags; this.tempfilesMasterSecret = tempfilesMasterSecret; } /** Create a MasterKeys with random keys. * @param random A secure RNG. Not specifically a SecureRandom because we want to be able to * use this in tests. */ public static MasterKeys createRandom(Random random) { byte[] clientCacheKey = new byte[32]; random.nextBytes(clientCacheKey); byte[] databaseKey = new byte[32]; random.nextBytes(databaseKey); byte[] tempfilesMasterSecret = new byte[64]; random.nextBytes(tempfilesMasterSecret); return new MasterKeys(clientCacheKey, databaseKey, tempfilesMasterSecret, 0); } void clearClientCacheKeys() { clear(clientCacheMasterKey); } static final int OLD_HASH_LENGTH = 4; static final int HASH_LENGTH = 12; static final int VERSION = 1; /** Sanity check */ static final long MAX_ITERATIONS = 1L << 40; /** Time in milliseconds to iterate for when encrypting a non-empty password. * FIXME make this configurable. FIXME Have a look at real password to key functions. */ static int ITERATE_TIME = 1000; public static MasterKeys read(File masterKeysFile, Random hardRandom, String password) throws MasterKeysWrongPasswordException, MasterKeysFileSizeException, IOException { System.err.println("Trying to read master keys file..."); if(masterKeysFile != null && masterKeysFile.exists()) { // Try to read the keys FileInputStream fis = null; // FIXME move declarations of sensitive data out and clear() in finally {} long len = masterKeysFile.length(); if(len > 1024) throw new MasterKeysFileSizeException(true); if(len < (32 + 32 + 8 + 32)) throw new MasterKeysFileSizeException(false); int length = (int) len; try { fis = new FileInputStream(masterKeysFile); DataInputStream dis = new DataInputStream(fis); if(len == 140) { MasterKeys ret = readOldFormat(dis, length, hardRandom, password); System.out.println("Read old-format master keys file. Writing new format master.keys ..."); ret.changePassword(masterKeysFile, password, hardRandom); return ret; } if(dis.readInt() != VERSION) throw new IOException("Bad version for master.keys"); long iterations = dis.readLong(); if(iterations < 0 || iterations > MAX_ITERATIONS) throw new IOException("Bad iterations "+iterations+" for master.keys"); byte[] salt = new byte[32]; dis.readFully(salt); byte[] iv = new byte[32]; dis.readFully(iv); byte[] dataAndHash = new byte[length - salt.length - iv.length - 4 - 8]; dis.readFully(dataAndHash); // System.err.println("Data and hash: "+HexUtil.bytesToHex(dataAndHash)); byte[] pwd = password.getBytes("UTF-8"); MessageDigest md = SHA256.getMessageDigest(); md.update(pwd); md.update(salt); byte[] outerKey = md.digest(); if(iterations > 0) { System.out.println("Decrypting master keys using password with "+iterations+" iterations..."); for(long i=0;i<iterations;i++) { md.update(salt); md.update(outerKey); outerKey = md.digest(); } } BlockCipher cipher; try { cipher = new Rijndael(256, 256); } catch (UnsupportedCipherException e) { // Impossible throw new Error(e); } // System.err.println("Outer key: "+HexUtil.bytesToHex(outerKey)); cipher.initialize(outerKey); PCFBMode pcfb = PCFBMode.create(cipher, iv); pcfb.blockDecipher(dataAndHash, 0, dataAndHash.length); // System.err.println("Decrypted data and hash: "+HexUtil.bytesToHex(dataAndHash)); byte[] data = Arrays.copyOf(dataAndHash, dataAndHash.length - HASH_LENGTH); byte[] hash = Arrays.copyOfRange(dataAndHash, data.length, dataAndHash.length); // System.err.println("Data: "+HexUtil.bytesToHex(data)); // System.err.println("Hash: "+HexUtil.bytesToHex(hash)); clear(dataAndHash); byte[] checkHash = md.digest(data); // System.err.println("Check hash: "+HexUtil.bytesToHex(checkHash)); if(!Fields.byteArrayEqual(checkHash, hash, 0, 0, HASH_LENGTH)) { clear(data); clear(hash); throw new MasterKeysWrongPasswordException(); } // It matches. Now decode it. ByteArrayInputStream bais = new ByteArrayInputStream(data); dis = new DataInputStream(bais); long flags = dis.readLong(); // At the moment there are no interesting flags. // In future the flags will tell us whether the database and the datastore are encrypted. byte[] clientCacheKey = new byte[32]; dis.readFully(clientCacheKey); byte[] databaseKey = null; databaseKey = new byte[32]; dis.readFully(databaseKey); byte[] tempfilesMasterSecret = new byte[64]; boolean mustWrite = false; if(data.length >= 8+32+32+64) { dis.readFully(tempfilesMasterSecret); } else { System.err.println("Created new master secret for encrypted tempfiles"); hardRandom.nextBytes(tempfilesMasterSecret); mustWrite = true; } MasterKeys ret = new MasterKeys(clientCacheKey, databaseKey, tempfilesMasterSecret, flags); clear(data); clear(hash); SHA256.returnMessageDigest(md); System.err.println("Read old master keys file"); if(mustWrite) { ret.changePassword(masterKeysFile, password, hardRandom); } return ret; } catch (FileNotFoundException e) { // Ok, create a new one. } catch (UnsupportedEncodingException e) { // Impossible System.err.println("JVM doesn't support UTF-8, this should be impossible!"); throw new Error(e); } catch (EOFException e) { throw new MasterKeysFileSizeException(false); } finally { Closer.close(fis); } } System.err.println("Creating new master keys file"); MasterKeys ret = createRandom(hardRandom); ret.write(masterKeysFile, password, hardRandom); return ret; } private static MasterKeys readOldFormat(DataInputStream dis, int length, Random hardRandom, String password) throws IOException, MasterKeysWrongPasswordException { byte[] salt = new byte[32]; dis.readFully(salt); byte[] iv = new byte[32]; dis.readFully(iv); byte[] dataAndHash = new byte[length - salt.length - iv.length]; dis.readFully(dataAndHash); // System.err.println("Data and hash: "+HexUtil.bytesToHex(dataAndHash)); byte[] pwd = password.getBytes("UTF-8"); MessageDigest md = SHA256.getMessageDigest(); md.update(pwd); md.update(salt); byte[] outerKey = md.digest(); BlockCipher cipher; try { cipher = new Rijndael(256, 256); } catch (UnsupportedCipherException e) { // Impossible throw new Error(e); } // System.err.println("Outer key: "+HexUtil.bytesToHex(outerKey)); cipher.initialize(outerKey); PCFBMode pcfb = PCFBMode.create(cipher, iv); pcfb.blockDecipher(dataAndHash, 0, dataAndHash.length); // System.err.println("Decrypted data and hash: "+HexUtil.bytesToHex(dataAndHash)); byte[] data = Arrays.copyOf(dataAndHash, dataAndHash.length - OLD_HASH_LENGTH); byte[] hash = Arrays.copyOfRange(dataAndHash, data.length, dataAndHash.length); // System.err.println("Data: "+HexUtil.bytesToHex(data)); // System.err.println("Hash: "+HexUtil.bytesToHex(hash)); clear(dataAndHash); byte[] checkHash = md.digest(data); // System.err.println("Check hash: "+HexUtil.bytesToHex(checkHash)); if(!Fields.byteArrayEqual(checkHash, hash, 0, 0, OLD_HASH_LENGTH)) { clear(data); clear(hash); throw new MasterKeysWrongPasswordException(); } // It matches. Now decode it. ByteArrayInputStream bais = new ByteArrayInputStream(data); dis = new DataInputStream(bais); // FIXME Fields.longToBytes and dis.readLong may not be compatible, find out if they are. byte[] flagsBytes = new byte[8]; dis.readFully(flagsBytes); long flags = Fields.bytesToLong(flagsBytes); // At the moment there are no interesting flags. // In future the flags will tell us whether the database and the datastore are encrypted. byte[] clientCacheKey = new byte[32]; dis.readFully(clientCacheKey); byte[] databaseKey = null; databaseKey = new byte[32]; dis.readFully(databaseKey); byte[] tempfilesMasterSecret = new byte[64]; System.err.println("Created new master secret for encrypted tempfiles"); hardRandom.nextBytes(tempfilesMasterSecret); MasterKeys ret = new MasterKeys(clientCacheKey, databaseKey, tempfilesMasterSecret, flags); clear(data); clear(hash); SHA256.returnMessageDigest(md); return ret; } public static void clear(byte[] buf) { if(buf == null) return; // Valid no-op, simplifies code Arrays.fill(buf, (byte)0x00); } public void changePassword(File masterKeysFile, String newPassword, Random hardRandom) throws IOException { System.err.println("Writing new master.keys file"); write(masterKeysFile, newPassword, hardRandom); } private void write(File masterKeysFile, String newPassword, Random hardRandom) throws IOException { // Write it to a byte[], check size, then replace in-place atomically // New IV, new salt, same client cache key, same database key ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] iv = new byte[32]; hardRandom.nextBytes(iv); byte[] salt = new byte[32]; hardRandom.nextBytes(salt); byte[] pwd; try { pwd = newPassword.getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { // Impossible throw new Error(e); } MessageDigest md = SHA256.getMessageDigest(); md.update(pwd); md.update(salt); byte[] outerKey = md.digest(); long iterations = 0; if(!newPassword.equals("")) { long startTime = System.currentTimeMillis(); while(System.currentTimeMillis() < startTime + ITERATE_TIME && iterations < MAX_ITERATIONS-20) { for(int i=0;i<10;i++) { iterations++; md.update(salt); md.update(outerKey); outerKey = md.digest(); } } System.out.println("Encrypted password with "+iterations+" iterations."); } DataOutputStream dos = new DataOutputStream(baos); dos.writeInt(VERSION); dos.writeLong(iterations); baos.write(salt); baos.write(iv); int hashedStart = salt.length + iv.length + 4 + 8; dos.writeLong(flags); baos.write(clientCacheMasterKey); baos.write(databaseKey); baos.write(tempfilesMasterSecret); byte[] data = baos.toByteArray(); md.update(data, hashedStart, data.length-hashedStart); byte[] hash = md.digest(); SHA256.returnMessageDigest(md); md = null; baos.write(hash, 0, HASH_LENGTH); data = baos.toByteArray(); BlockCipher cipher; try { cipher = new Rijndael(256, 256); } catch (UnsupportedCipherException e) { // Impossible throw new Error(e); } cipher.initialize(outerKey); PCFBMode pcfb = PCFBMode.create(cipher, iv); pcfb.blockEncipher(data, hashedStart, data.length - hashedStart); RandomAccessFile raf = new RandomAccessFile(masterKeysFile, "rw"); raf.seek(0); raf.write(data); long len = raf.length(); if(len > data.length) { byte[] diff = new byte[(int)(len - data.length)]; raf.write(diff); raf.setLength(data.length); } raf.getFD().sync(); raf.close(); } public static void killMasterKeys(File masterKeysFile) throws IOException { FileUtil.secureDelete(masterKeysFile); } public DatabaseKey createDatabaseKey(Random random) { return new DatabaseKey(databaseKey, random); } /** Used for creating keys for persistent encrypted tempfiles */ public MasterSecret getPersistentMasterSecret() { return new MasterSecret(tempfilesMasterSecret.clone()); } }