├── .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 |
--------------------------------------------------------------------------------