├── .gitignore ├── README.md ├── pom.xml └── src ├── main └── java │ └── com │ └── mlkui │ └── chrome │ ├── Aes256GcmHelper.java │ ├── ChromeDecryptHelper.java │ └── cookie │ └── entity │ ├── ChromeCookie.java │ ├── DecryptedCookie.java │ └── EncryptedCookie.java └── test └── java └── com └── mlkui └── chrome └── test └── ChromeCookieTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /.classpath 3 | /.project 4 | /bin/ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chrome-cookie-password-decryption 2 | The decryption implementation of Chrome cookie ('encrypted\_value' of the 'Cookies' SQLite file) or password ('password\_value' of the 'Login Data' SQLite file) on Windows. Both those which are not prefixed by 'v10' and those which are prefixed by 'v10' are supported. The codes in this repo are written in JDK1.8 and tested against Chrome 80.0.3987.106 x86 64bit on Windows 10 Professional 1903. 3 | 4 | The encrypted cookie and password are stored in SQLite file 'Cookies' and 'Login Data', which can be found in Chrome user data directory. Chrome user data directory is shown in [https://chromium.googlesource.com/chromium/src/+/master/docs/user\_data\_dir.md](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md). 5 | 6 | [https://github.com/n8henrie/pycookiecheat/issues/12](https://github.com/n8henrie/pycookiecheat/issues/12 "pycookiecheat") is a good place to learn how to find the symmetric key from keyring/keychain and decrypt cookie in Linux and Mac. However, pycookiecheat does not cover helpful information in Windows platform. 7 | 8 | We can understand how cookie values are encrypted from Chromium source code. I have written an article in Chinese at [http://www.meilongkui.com/archives/1904](http://www.meilongkui.com/archives/1904). In short, according to the version of Chrome, there are two different encryption methods: 9 | 10 | 1. encrypted values that are not prefixed by 'v10' or 'v11' 11 | 2. encrypted values that are prefixed by 'v10' or 'v11' 12 | 13 | If the encrypted values are not prefixed by 'v10' or 'v11', then Windows DPAPI (Data Protection Application Programming Interface) is used to encrypt the raw values. In theory, the Data Protection API can enable symmetric encryption of any kind of data; in practice, its primary use in the Windows operating system is to perform symmetric encryption of asymmetric private keys, using a user or system secret as a significant contribution of entropy. Actually, Chrome just uses DPAPI directly to get the encrpyted cookie values in this scenario. 14 | 15 | If the encrypted values are prefixed by 'v10' or 'v11', then AES-256-GCM AEAD algorithm is used to encrypt the raw values and the symmetric key is encrypted by DPAPI. The encrypted symmetric key is stored in 'Local State' file which is a big JSON text file. The symmetric key encrypted by DPAPI is located at 'os\_crypt.encrypted\_key' in Base64 format. The AES-256-GCM AEAD algorithm uses 256bit (32Byte) key and 96bit (12Byte) nonce/IV and 128bit (16*8) tag length. Each encrypted values are constructed by 3Bytes v10/v11 prefix followed by 12Bytes nonce/IV and the ciphertext. 16 | 17 | Because of the fact that DPAPI is hard to be handled directly for Windows platform and Java environment, we use windpapi4j. We use Bouncy Castle to implement AES-256-GCM instead of the native Java implementation which is limited by JCE. -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | chrome-cookie-password-decryption 7 | com.mlkui.chrome 8 | 0.0.1 9 | jar 10 | 11 | 12 | 13 | org.slf4j 14 | slf4j-api 15 | 1.7.19 16 | 17 | 18 | ch.qos.logback 19 | logback-classic 20 | 1.2.3 21 | 22 | 23 | org.slf4j 24 | jcl-over-slf4j 25 | 1.7.19 26 | 27 | 28 | org.slf4j 29 | log4j-over-slf4j 30 | 1.7.19 31 | 32 | 33 | 34 | org.apache.commons 35 | commons-lang3 36 | 3.3.2 37 | 38 | 39 | commons-io 40 | commons-io 41 | 2.4 42 | 43 | 44 | commons-codec 45 | commons-codec 46 | 1.14 47 | 48 | 49 | 50 | org.xerial 51 | sqlite-jdbc 52 | 3.23.1 53 | 54 | 55 | com.github.peter-gergely-horvath 56 | windpapi4j 57 | 1.0 58 | 59 | 60 | org.bouncycastle 61 | bcprov-jdk15on 62 | 1.60 63 | 64 | 65 | org.bouncycastle 66 | bcmail-jdk15on 67 | 1.60 68 | 69 | 70 | org.json 71 | org.json 72 | chargebee-1.0 73 | 74 | 75 | 76 | junit 77 | junit 78 | 4.13.1 79 | test 80 | 81 | 82 | 83 | 84 | src 85 | 86 | 87 | maven-compiler-plugin 88 | 3.1 89 | 90 | UTF-8 91 | 1.8 92 | 1.8 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /src/main/java/com/mlkui/chrome/Aes256GcmHelper.java: -------------------------------------------------------------------------------- 1 | package com.mlkui.chrome; 2 | 3 | import java.security.Security; 4 | 5 | import javax.crypto.Cipher; 6 | import javax.crypto.spec.GCMParameterSpec; 7 | import javax.crypto.spec.SecretKeySpec; 8 | 9 | import org.bouncycastle.jce.provider.BouncyCastleProvider; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | //EVP_aead_aes_256_gcm 14 | //The same algorithm of the cookie and password encryption in Chrome (prefixed by v10) 15 | public class Aes256GcmHelper 16 | { 17 | private static Logger logger = LoggerFactory.getLogger(Aes256GcmHelper.class); 18 | 19 | private static final int KEY_LENGTH = 256 / 8; 20 | private static final int IV_LENGTH = 96 / 8; 21 | private static final int GCM_TAG_LENGTH = 16; 22 | 23 | static 24 | { 25 | Security.addProvider(new BouncyCastleProvider()); 26 | } 27 | 28 | public static final byte[] getEncryptedBytes(byte[] inputBytes, byte[] keyBytes, byte[] ivBytes) 29 | { 30 | try 31 | { 32 | if (inputBytes == null) 33 | { 34 | throw new IllegalArgumentException(); 35 | } 36 | 37 | if (keyBytes == null) 38 | { 39 | throw new IllegalArgumentException(); 40 | } 41 | if (keyBytes.length != KEY_LENGTH) 42 | { 43 | throw new IllegalArgumentException(); 44 | } 45 | 46 | if (ivBytes == null) 47 | { 48 | throw new IllegalArgumentException(); 49 | } 50 | if (ivBytes.length != IV_LENGTH) 51 | { 52 | throw new IllegalArgumentException(); 53 | } 54 | 55 | Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); 56 | SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "AES"); 57 | GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, ivBytes); 58 | cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmParameterSpec); 59 | return cipher.doFinal(inputBytes); 60 | } 61 | catch (Exception ex) 62 | { 63 | logger.error(ex.toString(), ex.fillInStackTrace()); 64 | return null; 65 | } 66 | } 67 | 68 | public static final byte[] getDecryptBytes(byte[] inputBytes, byte[] keyBytes, byte[] ivBytes) 69 | { 70 | try 71 | { 72 | if (inputBytes == null) 73 | { 74 | throw new IllegalArgumentException(); 75 | } 76 | 77 | if (keyBytes == null) 78 | { 79 | throw new IllegalArgumentException(); 80 | } 81 | if (keyBytes.length != KEY_LENGTH) 82 | { 83 | throw new IllegalArgumentException(); 84 | } 85 | 86 | if (ivBytes == null) 87 | { 88 | throw new IllegalArgumentException(); 89 | } 90 | if (ivBytes.length != IV_LENGTH) 91 | { 92 | throw new IllegalArgumentException(); 93 | } 94 | 95 | Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); 96 | SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "AES"); 97 | GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, ivBytes); 98 | cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, gcmParameterSpec); 99 | return cipher.doFinal(inputBytes); 100 | } 101 | catch (Exception ex) 102 | { 103 | logger.error(ex.toString(), ex.fillInStackTrace()); 104 | return null; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/mlkui/chrome/ChromeDecryptHelper.java: -------------------------------------------------------------------------------- 1 | package com.mlkui.chrome; 2 | 3 | import java.io.File; 4 | import java.sql.Connection; 5 | import java.sql.DriverManager; 6 | import java.sql.ResultSet; 7 | import java.sql.SQLException; 8 | import java.sql.Statement; 9 | import java.util.Arrays; 10 | import java.util.Date; 11 | import java.util.HashSet; 12 | import java.util.Set; 13 | import java.util.UUID; 14 | 15 | import org.apache.commons.codec.binary.Base64; 16 | import org.apache.commons.io.FileUtils; 17 | import org.apache.commons.lang3.StringUtils; 18 | import org.json.JSONObject; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | 22 | import com.github.windpapi4j.WinDPAPI; 23 | import com.github.windpapi4j.WinDPAPI.CryptProtectFlag; 24 | import com.mlkui.chrome.cookie.entity.ChromeCookie; 25 | import com.mlkui.chrome.cookie.entity.DecryptedCookie; 26 | import com.mlkui.chrome.cookie.entity.EncryptedCookie; 27 | 28 | public class ChromeDecryptHelper 29 | { 30 | private static final int QUERY_TIMEOUT = 30; 31 | 32 | private static final int kKeyLength = 256 / 8; 33 | private static final int kNonceLength = 96 / 8; 34 | private static final String kEncryptionVersionPrefix = "v10"; 35 | private static final String kDPAPIKeyPrefix = "DPAPI"; 36 | 37 | private Logger logger = LoggerFactory.getLogger(this.getClass()); 38 | 39 | private String cookieFileFullPathAndName; 40 | private String localStateFileFullPathAndName; 41 | 42 | public ChromeDecryptHelper(String cookieFileFullPathAndName) 43 | { 44 | this.cookieFileFullPathAndName = cookieFileFullPathAndName; 45 | } 46 | 47 | public ChromeDecryptHelper(String cookieFileFullPathAndName, String localStateFileFullPathAndName) 48 | { 49 | this.cookieFileFullPathAndName = cookieFileFullPathAndName; 50 | this.localStateFileFullPathAndName = localStateFileFullPathAndName; 51 | } 52 | 53 | public String getCookieFileFullPathAndName() 54 | { 55 | return cookieFileFullPathAndName; 56 | } 57 | 58 | public String getLocalStateFileFullPathAndName() 59 | { 60 | return localStateFileFullPathAndName; 61 | } 62 | 63 | public Set getDecryptedCookies() 64 | { 65 | HashSet cookieSet = new HashSet<>(); 66 | 67 | File cookieFile = new File(cookieFileFullPathAndName); 68 | if (!cookieFile.exists()) 69 | { 70 | return cookieSet; 71 | } 72 | 73 | Connection connection = null; 74 | try 75 | { 76 | File tempFile = new File(UUID.randomUUID().toString()); 77 | FileUtils.copyFile(cookieFile, tempFile); 78 | 79 | Class.forName("org.sqlite.JDBC"); 80 | connection = DriverManager.getConnection("jdbc:sqlite:" + tempFile.getAbsolutePath()); 81 | Statement statement = connection.createStatement(); 82 | statement.setQueryTimeout(QUERY_TIMEOUT); 83 | 84 | ResultSet resultSet = statement.executeQuery("SELECT * FROM cookies"); 85 | 86 | while (resultSet.next()) 87 | { 88 | String name = resultSet.getString("name"); 89 | parseCookieFromResult(tempFile, name, cookieSet, resultSet); 90 | } 91 | } 92 | catch (Exception ex) 93 | { 94 | logger.error(ex.toString(), ex.fillInStackTrace()); 95 | } 96 | finally 97 | { 98 | try 99 | { 100 | if (connection != null) 101 | { 102 | connection.close(); 103 | } 104 | } 105 | catch (Exception e) 106 | { 107 | } 108 | } 109 | 110 | return cookieSet; 111 | } 112 | 113 | private void parseCookieFromResult(File cookieStore, String name, HashSet cookieSet, ResultSet resultSet) throws SQLException 114 | { 115 | byte[] encryptedBytes = resultSet.getBytes("encrypted_value"); 116 | String path = resultSet.getString("path"); 117 | String domain = resultSet.getString("host_key"); 118 | boolean secure = resultSet.getBoolean("is_secure"); 119 | boolean httpOnly = resultSet.getBoolean("is_httponly"); 120 | Date expires = resultSet.getDate("expires_utc"); 121 | 122 | EncryptedCookie encryptedCookie = new EncryptedCookie(name, encryptedBytes, expires, path, domain, secure, httpOnly, cookieStore); 123 | DecryptedCookie decryptedCookie = decrypt(encryptedCookie); 124 | if (decryptedCookie != null) 125 | { 126 | cookieSet.add(decryptedCookie); 127 | } 128 | else 129 | { 130 | cookieSet.add(encryptedCookie); 131 | } 132 | } 133 | 134 | private DecryptedCookie decrypt(EncryptedCookie encryptedCookie) 135 | { 136 | byte[] decryptedBytes = null; 137 | 138 | byte[] encryptedValue = encryptedCookie.getEncryptedValue(); 139 | try 140 | { 141 | boolean isV10 = new String(encryptedValue).startsWith("v10"); 142 | if (WinDPAPI.isPlatformSupported()) 143 | { 144 | WinDPAPI winDPAPI = WinDPAPI.newInstance(CryptProtectFlag.CRYPTPROTECT_UI_FORBIDDEN); 145 | 146 | if (!isV10) 147 | { 148 | decryptedBytes = winDPAPI.unprotectData(encryptedValue); 149 | } 150 | else 151 | { 152 | if (StringUtils.isEmpty(localStateFileFullPathAndName)) 153 | { 154 | throw new IllegalArgumentException("Local State is required"); 155 | } 156 | 157 | // Retrieve the AES key which is encrypted by DPAPI from Local State 158 | String localState = FileUtils.readFileToString(new File(this.localStateFileFullPathAndName)); 159 | JSONObject jsonObject = new JSONObject(localState); 160 | String encryptedKeyBase64 = jsonObject.getJSONObject("os_crypt").getString("encrypted_key"); 161 | byte[] encryptedKeyBytes = Base64.decodeBase64(encryptedKeyBase64); 162 | if (!new String(encryptedKeyBytes).startsWith(kDPAPIKeyPrefix)) 163 | { 164 | throw new IllegalStateException("Local State should start with DPAPI"); 165 | } 166 | encryptedKeyBytes = Arrays.copyOfRange(encryptedKeyBytes, kDPAPIKeyPrefix.length(), encryptedKeyBytes.length); 167 | 168 | // Use DPAPI to get the real AES key 169 | byte[] keyBytes = winDPAPI.unprotectData(encryptedKeyBytes); 170 | if (keyBytes.length != kKeyLength) 171 | { 172 | throw new IllegalStateException("Local State key length is wrong"); 173 | } 174 | 175 | // Obtain the nonce. 176 | byte[] nonceBytes = Arrays.copyOfRange(encryptedValue, kEncryptionVersionPrefix.length(), kEncryptionVersionPrefix.length() + kNonceLength); 177 | 178 | // Strip off the versioning prefix before decrypting. 179 | encryptedValue = Arrays.copyOfRange(encryptedValue, kEncryptionVersionPrefix.length() + kNonceLength, encryptedValue.length); 180 | 181 | // Use BC provider to decrypt 182 | decryptedBytes = Aes256GcmHelper.getDecryptBytes(encryptedValue, keyBytes, nonceBytes); 183 | } 184 | } 185 | } 186 | catch (Exception e) 187 | { 188 | logger.error(e.toString(), e.fillInStackTrace()); 189 | return null; 190 | } 191 | 192 | return new DecryptedCookie(encryptedCookie.getName(), encryptedValue, new String(decryptedBytes), encryptedCookie.getExpires(), encryptedCookie.getPath(), encryptedCookie.getDomain(), encryptedCookie.isSecure(), encryptedCookie.isHttpOnly(), encryptedCookie.getCookieStore()); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/main/java/com/mlkui/chrome/cookie/entity/ChromeCookie.java: -------------------------------------------------------------------------------- 1 | package com.mlkui.chrome.cookie.entity; 2 | 3 | import java.io.File; 4 | import java.util.Date; 5 | 6 | public class ChromeCookie 7 | { 8 | protected String name; 9 | protected String value; 10 | protected Date expires; 11 | protected String path; 12 | protected String domain; 13 | protected boolean secure; 14 | protected boolean httpOnly; 15 | protected File cookieStore; 16 | 17 | public ChromeCookie(String name, String value, Date expires, String path, String domain, boolean secure, boolean httpOnly, File cookieStore) 18 | { 19 | this.name = name; 20 | this.value = value; 21 | this.expires = expires; 22 | this.path = path; 23 | this.domain = domain; 24 | this.secure = secure; 25 | this.httpOnly = httpOnly; 26 | this.cookieStore = cookieStore; 27 | } 28 | 29 | public ChromeCookie(String name, Date expires, String path, String domain, boolean secure, boolean httpOnly, File cookieStore) 30 | { 31 | this.name = name; 32 | this.expires = expires; 33 | this.path = path; 34 | this.domain = domain; 35 | this.secure = secure; 36 | this.httpOnly = httpOnly; 37 | this.cookieStore = cookieStore; 38 | } 39 | 40 | public String getName() 41 | { 42 | return name; 43 | } 44 | 45 | public String getValue() 46 | { 47 | return value; 48 | } 49 | 50 | public Date getExpires() 51 | { 52 | return expires; 53 | } 54 | 55 | public String getPath() 56 | { 57 | return path; 58 | } 59 | 60 | public String getDomain() 61 | { 62 | return domain; 63 | } 64 | 65 | public boolean isSecure() 66 | { 67 | return secure; 68 | } 69 | 70 | public boolean isHttpOnly() 71 | { 72 | return httpOnly; 73 | } 74 | 75 | public File getCookieStore() 76 | { 77 | return cookieStore; 78 | } 79 | 80 | @Override 81 | public String toString() 82 | { 83 | return "Cookie [name=" + name + ", value=" + value + "]"; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/mlkui/chrome/cookie/entity/DecryptedCookie.java: -------------------------------------------------------------------------------- 1 | package com.mlkui.chrome.cookie.entity; 2 | 3 | import java.io.File; 4 | import java.util.Date; 5 | 6 | public class DecryptedCookie extends EncryptedCookie 7 | { 8 | protected String decryptedValue; 9 | 10 | public DecryptedCookie(String name, byte[] encryptedValue, String decryptedValue, Date expires, String path, String domain, boolean secure, boolean httpOnly, File cookieStore) 11 | { 12 | super(name, encryptedValue, expires, path, domain, secure, httpOnly, cookieStore); 13 | this.decryptedValue = decryptedValue; 14 | } 15 | 16 | public String getDecryptedValue() 17 | { 18 | return decryptedValue; 19 | } 20 | 21 | @Override 22 | public boolean isDecrypted() 23 | { 24 | return true; 25 | } 26 | 27 | @Override 28 | public String toString() 29 | { 30 | return "Cookie [name=" + name + ", value=" + decryptedValue + "]"; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/mlkui/chrome/cookie/entity/EncryptedCookie.java: -------------------------------------------------------------------------------- 1 | package com.mlkui.chrome.cookie.entity; 2 | 3 | import java.io.File; 4 | import java.util.Date; 5 | 6 | public class EncryptedCookie extends ChromeCookie 7 | { 8 | protected byte[] encryptedValue; 9 | 10 | public byte[] getEncryptedValue() 11 | { 12 | return encryptedValue; 13 | } 14 | 15 | public EncryptedCookie(String name, byte[] encryptedValue, Date expires, String path, String domain, boolean secure, boolean httpOnly, File cookieStore) 16 | { 17 | super(name, expires, path, domain, secure, httpOnly, cookieStore); 18 | this.encryptedValue = encryptedValue; 19 | } 20 | 21 | public boolean isDecrypted() 22 | { 23 | return false; 24 | } 25 | 26 | @Override 27 | public String toString() 28 | { 29 | return "Cookie [name=" + name + " (encrypted)]"; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/mlkui/chrome/test/ChromeCookieTest.java: -------------------------------------------------------------------------------- 1 | package com.mlkui.chrome.test; 2 | 3 | import java.util.Set; 4 | 5 | import org.junit.Test; 6 | 7 | import com.mlkui.chrome.ChromeDecryptHelper; 8 | import com.mlkui.chrome.cookie.entity.ChromeCookie; 9 | 10 | public class ChromeCookieTest 11 | { 12 | @Test 13 | public void test() 14 | { 15 | String cookieFileFullPathAndName = "D:\\chrome\\test-user-data\\Default\\Cookies"; 16 | String localStateFileFullPathAndName = "D:\\chrome\\test-user-data\\Local State"; 17 | ChromeDecryptHelper chromeDecryptHelper = new ChromeDecryptHelper(cookieFileFullPathAndName, localStateFileFullPathAndName); 18 | Set chromeCookies = chromeDecryptHelper.getDecryptedCookies(); 19 | for (ChromeCookie chromeCookie : chromeCookies) 20 | { 21 | System.out.println(chromeCookie); 22 | } 23 | } 24 | } 25 | --------------------------------------------------------------------------------