package com.frontier42.keepass.impl;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.DigestOutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.UUID;
import java.util.zip.GZIPInputStream;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import org.bouncycastle.crypto.StreamCipher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.keepassdroid.crypto.CipherFactory;
import com.keepassdroid.crypto.PwStreamCipherFactory;
import com.keepassdroid.crypto.finalkey.FinalKey;
import com.keepassdroid.crypto.finalkey.FinalKeyFactory;
import com.keepassdroid.database.PwCompressionAlgorithm;
import com.keepassdroid.database.exception.InvalidDBVersionException;
import com.keepassdroid.database.exception.InvalidPasswordException;
import com.keepassdroid.stream.BetterCipherInputStream;
import com.keepassdroid.stream.LEDataInputStream;
import com.keepassdroid.stream.NullOutputStream;
public class DatabaseReaderV4 {
private final Logger LOG=LoggerFactory.getLogger(getClass());
protected UUID dataCipher;
protected PwCompressionAlgorithm compressionAlgorithm;
protected long numKeyEncRounds;
protected StreamCipher randomStream;
public byte[] makeFinalKey(byte masterKey[], byte[] masterSeed,
byte[] masterSeed2, int numRounds) throws IOException {
// Write checksum Checksum
MessageDigest md = null;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IOException("SHA-256 not implemented here.");
}
NullOutputStream nos = new NullOutputStream();
DigestOutputStream dos = new DigestOutputStream(nos, md);
byte[] transformedMasterKey = transformMasterKey(masterSeed2,
masterKey, numRounds);
dos.write(masterSeed);
dos.write(transformedMasterKey);
return md.digest();
}
/**
* Encrypt the master key a few times to make brute-force key-search harder
*
* @throws IOException
*/
private static byte[] transformMasterKey(byte[] pKeySeed, byte[] pKey,
int rounds) throws IOException {
FinalKey key = FinalKeyFactory.createFinalKey();
return key.transformMasterKey(pKeySeed, pKey, rounds);
}
protected byte[] getPasswordKey(String key, String encoding)
throws IOException {
assert (key != null);
if (key.length() == 0)
throw new IllegalArgumentException("Key cannot be empty.");
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IOException("SHA-256 not supported");
}
byte[] bKey;
try {
bKey = key.getBytes(encoding);
} catch (UnsupportedEncodingException e) {
assert false;
bKey = key.getBytes();
}
md.update(bKey, 0, bKey.length);
return md.digest();
}
public byte[] getPasswordKey(String key) throws IOException {
return getPasswordKey(key, "UTF-8");
}
public byte[] getMasterKey(String key) throws IOException {
byte[] fKey;
fKey = getPasswordKey(key);
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IOException("No SHA-256 implementation");
}
return md.digest(fKey);
}
public InputStream openDecryptedStrem(InputStream is, String password) throws IOException, InvalidDBVersionException, InvalidPasswordException{
// BufferedInputStream bis = new BufferedInputStream(is);
/*
* if ( ! bis.markSupported() ) { throw new
* IOException("Input stream does not support mark."); }
*/
// We'll end up reading 8 bytes to identify the header. Might as well
// use two extra.
// bis.mark(10);
DatabaseHeaderV4 header = new DatabaseHeaderV4(this);
header.load(is);
byte[] finalKey = makeFinalKey(getMasterKey(password),
header.masterSeed, header.transformSeed,
(int) this.numKeyEncRounds);
// bis.reset();
// bis.skip(8);
// Attach decryptor
Cipher cipher;
try {
cipher = CipherFactory.getInstance(this.dataCipher,
Cipher.DECRYPT_MODE, finalKey, header.encryptionIV);
} catch (NoSuchAlgorithmException e) {
throw new IOException("Invalid algorithm.");
} catch (NoSuchPaddingException e) {
throw new IOException("Invalid algorithm.");
} catch (InvalidKeyException e) {
throw new IOException("Invalid algorithm.");
} catch (InvalidAlgorithmParameterException e) {
throw new IOException("Invalid algorithm.");
}
InputStream decrypted = new BetterCipherInputStream(is, cipher, 50 * 1024);
LEDataInputStream dataDecrypted = new LEDataInputStream(decrypted);
byte[] storedStartBytes = null;
try {
storedStartBytes = dataDecrypted.readBytes(32);
if (storedStartBytes == null || storedStartBytes.length != 32) {
throw new InvalidPasswordException();
}
} catch (IOException e) {
throw new InvalidPasswordException();
}
if (!Arrays.equals(storedStartBytes, header.streamStartBytes)) {
throw new InvalidPasswordException();
}
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
OutputStream data=byteArrayOutputStream;
boolean bexit=false;
while (!bexit){
int blockId=dataDecrypted.readInt();
byte[] blockHash=dataDecrypted.readBytes(32);
int blockSize=dataDecrypted.readInt();
if (blockSize>0){
data.write(dataDecrypted.readBytes(blockSize));
}else{
bexit=true;
}
}
//System.err.println(byteArrayOutputStream.toString());
InputStream hashed = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
InputStream decompressed;
if (this.compressionAlgorithm == PwCompressionAlgorithm.Gzip) {
decompressed = new GZIPInputStream(hashed);
} else {
decompressed = hashed;
}
if (header.protectedStreamKey == null) {
assert (false);
throw new IOException("Invalid stream key.");
}
LOG.info("CipherKey:{}", header.protectedStreamKey);
randomStream = new StreamCipherDelegator(PwStreamCipherFactory.getInstance(
header.innerRandomStream, header.protectedStreamKey));
return decompressed;
}
public StreamCipher getRandomStreamCipher() {
return randomStream;
}
}