This paper is written from the view of a programmer. It describes which algorithms are used by Mozilla to encrypt login data, i.e. saved passwords and usernames for websites in Firefox or the login data of your e-mail accounts in Thunderbird. I will provide some example code (Java) from my
MozillaRecovery program.
How I got the information: (skip this, if you only want the information itself)
An information that you will find without problems is the location of your login data: It is the signons.sqlite (or signons.txt, signons3.txt in older versions), which can be found in the profile folder of your application.
First thing I did was researching about the sqlite format:
http://www.sqlite.org/fileformat2.htmlIt is recommended to use a hex editor to compare the description with your own signons.sqlite file.
The format is well documented, so writing a program that obtains data from an sqlite file shouldn't be a problem.
Because I read that the data is encoded in Base64 and not encrypted if no master password is set, I copied a username entry and tried to decode. But it didn't work. I guess it worked with older versions. Now there is some kind of encryption too.
I searched for open-source programs that recover passwords from Firefox or Thunderbird and found this:
http://securityxploded.com/thunderbirdpassdecryptor.phpOld website entries told me it was open-source, but I couldn't find any source to download. Old postings in the forum of securityxploded told me, that they changed this. Some people had used their code for writing maleware, so antivirus scanner recognized their program as a virus. It is pretty sad that the lazyness (not writing their own code, just grieving) and improvidence of some people forced the authors of ThunderbirdPassDecryptor to hide their knowledge. The further search for open-source programs was not fruitful.
In fact, signons.sqlite is useless without the key3.db file, which also resides in the profile folder of your application. This is where the trouble began. I couldn't find information about that file for a long time, so I downloaded the source code of Thunderbird, looked into it for several days and learned more about it's inner workings. I discovered that the login data in the signons.sqlite file is encrypted with TripleDES in CBC mode. The key used for the encryption is saved in key3.db and encrypted as well.
One day I stumbled on this website and it helped me a lot:
http://www.drh-consultancy.demon.co.uk/key3.htmlIt describes how the keys in key3.db can be obtained. But not everything is correct anymore. Some changes are necessary.
First thing that made me think:
Initially you will need the database password
Where do I get that from?
I just guessed that this is the master password and was right.
I also got the idea that the entry values should follow right after the entry name (I am not sure if it is standard knowledge to do it in another way). I.e. looking at the key3.db in a hex editor you might get that picture on the plain text side:
...................password-check.Version..........Which means the password-check entry would only have a one byte value. That couldn't be true. But the version entry which follows right after, only has a one byte value. So I tried it backwards, with the entry name following it's value (which lead to the problem to find out where the entries start). It was still not enough to get it working.
Since this website provides some test vectors (I am very grateful for that), I was able to implement and verify the decryption algorithm. Now I knew that it worked with the data on this website, but it still didn't work with my own key3.db file.
I can't really say how I got the idea, but I changed the length of the global salt entry from 16 bytes to 20 bytes. I guess it was just out of a hunch while looking at the hex values. Surprisingly this was the right thing. My test output decrypted the string "password-check" and I was happy. This is how I got the main algorithm for checking if a master password is the right one.
I still didn't implement a program for obtaining the login data out of signons.sqlite, once you got the key entries from key3.db. But my hunger for knowing how it works is satisfied and implementing it shouldn't be necessary at all. Reason: Thunderbird and Firefox show you the data (passwords included) in plaintext, if you know the master password. If no master password is given, the data is not secured at all, just encrypted with a hardcoded key:
http://www.infond.fr/2010/04/firefox-passwords-management-leaks.html(I didn't verify this yet, but I will)
How Mozilla saves login data:Summary: login data is saved in signons.sqlite. It is encoded in Base64, encrypted with TripleDES in CBC mode and standard block padding. The key for the decryption is saved in key3.db. The entries in key3.db are encrypted with the master password. The decryption algorithm (of the key3.db entries) is not straight forward, but shown right after.
Sqlite Format: http://www.sqlite.org/fileformat2.htmlNetscape Communicator Key Database Format: http://www.drh-consultancy.demon.co.uk/key3.htmlWork through this description, but change the following:
- the global salt value is 20 bytes (not 16 bytes) long (I think there may be a value indicating the length of the global salt somewhere)
- the plain text entry names (i.e. Version, global salt) follow after their values
- the database password is the master password
To verify the master password and your decryption algorithm, use the check-password entry. Its value is the encrypted string "check-password".
Java example code: extracted from
MozillaRecovery Key3.db key derivation algorithm:The comments are in the notation of the website mentioned above.
private static String decrypt(byte[] password, byte[] es, byte[] gs, byte[] text) {
try {
// HP = SHA1(global-salt||password)
byte[] hp = SHA.sha1(appendArray(gs, password));
byte[] pes = Arrays.copyOf(es, 20);
// CHP = SHA1(HP||ES)
byte[] chp = SHA.sha1(appendArray(hp, es));
// k1 = CHMAC(PES||ES)
byte[] k1 = SHA.sha1Hmac(appendArray(pes, es), chp);
// tk = CHMAC(PES)
byte[] tk = SHA.sha1Hmac(pes, chp);
// k2 = CHMAC(tk||ES)
byte[] k2 = SHA.sha1Hmac(appendArray(tk, es), chp);
// k = k1||k2
byte[] k = appendArray(k1, k2);
byte[] desKey = Arrays.copyOf(k, 24);
byte[] desIV = Arrays.copyOfRange(k, k.length - 8, k.length);
return new TripleDES(desKey, desIV).decrypt(text);
} catch (NoSuchAlgorithmException e) {
logger.fatal(e.getMessage());
e.printStackTrace();
} catch (BadPaddingException e) {
logger.debug(e.getMessage() + ". Probably wrong key.");
}
return null;
}
SHA-1 and HMAC-SHA1:import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class SHA {
private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
private static final String SHA1_ALGORITHM = "SHA-1";
public static byte[] sha1Hmac(byte[] data, byte[] key) {
try {
SecretKeySpec signingKey = new SecretKeySpec(key,
HMAC_SHA1_ALGORITHM);
Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
mac.init(signingKey);
return mac.doFinal(data);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
e.printStackTrace();
}
return null;
}
public static byte[] sha1(byte[] text) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance(SHA1_ALGORITHM);
md.update(text, 0, text.length);
return md.digest();
}
}}
TripleDES:import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
import javax.crypto.spec.IvParameterSpec;
public class TripleDES {
private KeySpec keySpec;
private SecretKey key;
private IvParameterSpec iv;
public TripleDES(byte[] keyBytes, byte[] ivString) {
try {
keySpec = new DESedeKeySpec(keyBytes);
key = SecretKeyFactory.getInstance("DESede")
.generateSecret(keySpec);
iv = new IvParameterSpec(ivString);
} catch (InvalidKeySpecException | NoSuchAlgorithmException
| InvalidKeyException e) {
e.printStackTrace();
}
}
public byte[] encrypt(byte[] text) {
if (text != null) {
try {
Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding",
"SunJCE");
cipher.init(Cipher.ENCRYPT_MODE, key, iv);
return cipher.doFinal(text);
} catch (IllegalBlockSizeException | InvalidKeyException
| InvalidAlgorithmParameterException
| NoSuchAlgorithmException | NoSuchProviderException
| NoSuchPaddingException | BadPaddingException e) {
e.printStackTrace();
}
}
return null;
}
public String decrypt(byte[] text) throws BadPaddingException {
if (text != null) {
try {
Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding",
"SunJCE");
cipher.init(Cipher.DECRYPT_MODE, key, iv);
byte[] result = cipher.doFinal(text);
return new String(result, "UTF8");
} catch (NoSuchAlgorithmException | NoSuchProviderException
| NoSuchPaddingException | IllegalBlockSizeException
| InvalidKeyException | InvalidAlgorithmParameterException
| UnsupportedEncodingException e) {
e.printStackTrace();
}
}
return null;
}
}