Skip to content

Commit 62d3772

Browse files
authored
Merge pull request #8 from spak9/encryption
Encryption
2 parents f1a089d + 7e27588 commit 62d3772

15 files changed

+374
-76
lines changed

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# mmkv_visualizer
22
A web application that will allow you to visualize [MMKV](https://github.com/Tencent/MMKV) databases, with all processing done client-side.
3-
The [web service](https://www.mmkv-visualizer.com/) utilizes [Pyodide](https://pyodide.org/en/stable/) enables a python
3+
The [web service](https://www.mmkv-visualizer.com/) utilizes [Pyodide](https://pyodide.org/en/stable/) which enables a python
44
runtime within the browser, in which the main MMKV parsing code is written in.
55
It sends no data up to any server and all the parsing happens right in your browser.
66

@@ -13,7 +13,7 @@ There are three ways you can use the following code:
1313
The main way is to utilize the online web service provided at https://www.mmkv-visualizer.com/.
1414
You can simply drag & drop or choose an MMKV of your choice, then visualize the data.
1515
The visualizer allows you to iterate through different data type encodings (MMKV uses protobuf encoding)
16-
by simply clicking any table cell, as well as expand the data to get a deeper look.
16+
by simply clicking any table cell, as well as expand the data to get a deeper look.
1717

1818
2. Local Web Application:
1919

@@ -30,8 +30,16 @@ does not allow you to see older data, while this parser can. This may be importa
3030

3131
You can also find a set of python tests found at `tests`.
3232

33+
## Decryption
34+
35+
The MMKV library natives allows users to encrypt their MMKV files using AES-128 in CFB mode.
36+
If needed, the application allows decryption of the data, but must be given the following:
37+
38+
1. The encrypted MMKV file AND corresponding .crc file (the .crc file contains the 16-byte IV)
39+
2. The AES key. (Will be prompted to enter the AES key in the form of a hexstring, allowing any length for the key)
40+
3341
## Roadmap
3442

35-
- [ ] Expose more metadata: whether database size was found, or if it's a best effort approach, file is empty, file is probablistically encrypted.
43+
- [x] Expose more metadata: whether database size was found, or if it's a best effort approach, file is empty, file is probablistically encrypted.
3644
- [ ] Experiment with new UI design and see if Svelte Material is a viable solution.
37-
- [ ] Enable decryption with user input of encryption key.
45+
- [x] Enable decryption with user input of encryption key.

frontend/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<html lang="en">
33
<head>
44
<title>MMKV Visualizer</title>
5+
<meta name="description" content="MMKV Viewer that allows visualization of the most recent key-value pairs, as well as older logged values not available via the native API">
56
<link rel="stylesheet" href="/src/app.css">
67
<!-- Load in Pyodide Script -->
78
<script src="https://cdn.jsdelivr.net/pyodide/v0.21.3/full/pyodide.js"></script>

frontend/public/mmkv_parser.py

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from pathlib import Path
33
from typing import Optional, List, Union, Tuple, DefaultDict
44
from collections import defaultdict
5+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
56

67
import sys
78
import struct
@@ -166,29 +167,26 @@ def __init__(self, mmkv_file_data: Union[str, BufferedIOBase], crc_file_data: Un
166167
else:
167168
pass
168169

169-
170170
# Initialize our files
171171
self.mmkv_file: BufferedIOBase = mmkv_file_data
172172
self.crc_file: Optional[BufferedIOBase] = crc_file_data
173173
self.pos: int = 0
174174
self.decoded_map: DefaultDict[str, List[bytes]] = defaultdict(list)
175175

176-
# Read in first 4 header bytes - [0:4] is total size
177-
self.header_bytes: bytes = self.mmkv_file.read(4)
178-
if len(self.header_bytes) != 4:
179-
raise ValueError('[+] Error while reading mmkv_file. Header bytes was not 4 bytes.')
180-
self.pos += 4
181-
182-
# TODO: find out the purpose of the varint in [4:x] position
183-
# [4:X] is garbage bytes basically (0xffffff07) or is another varint
184-
x, bytes_read = decode_unsigned_varint(self.mmkv_file)
185-
if (x, bytes_read) == (-1, -1):
186-
raise ValueError('[+] Error while decoding the [4:X] bytes of the mmkv_file.')
176+
# Found IV from .crc file - don't read anything from the stream if encrypted
177+
if self.crc_file:
178+
crc_header_bytes = self.crc_file.read(28)
179+
if len(crc_header_bytes) != 28:
180+
raise ValueError('[+] Error while reading crc_file. Header bytes was not 28 bytes.')
181+
self.iv = crc_header_bytes[12:28]
187182

188-
self.pos += bytes_read
183+
# Cannot find IV from .crc file - prepare stream for decoding into a map
184+
else:
185+
print('[+] .CRC file was not passed in - is needed for decryption routines')
186+
self.iv = b''
189187

190188

191-
def get_db_size(self) -> int:
189+
def _get_db_size(self) -> int:
192190
"""
193191
Returns the actual size known to the MMKV API for querying data. This includes older
194192
logged data that the actual MMKV API does not have the ability to query.
@@ -207,6 +205,59 @@ def get_db_size(self) -> int:
207205
raise TypeError(f'[+] Error while unpacking header bytes. Received {type(size)}')
208206

209207

208+
def _prepare_mmkv_stream_for_decoding(self):
209+
# Read in first 4 header bytes - [0:4] is total size
210+
self.header_bytes: bytes = self.mmkv_file.read(4)
211+
if len(self.header_bytes) != 4:
212+
raise ValueError('[+] Error while reading mmkv_file. Header bytes was not 4 bytes.')
213+
self.pos += 4
214+
215+
# TODO: find out the purpose of the varint in [4:x] position
216+
# [4:X] is garbage bytes basically (0xffffff07) or is another varint
217+
x, bytes_read = decode_unsigned_varint(self.mmkv_file)
218+
if (x, bytes_read) == (-1, -1):
219+
raise ValueError('[+] Error while decoding the [4:X] bytes of the mmkv_file.')
220+
221+
self.pos += bytes_read
222+
223+
224+
def decrypt_and_reconstruct(self, key: Union[str, bytes]) -> bytes:
225+
"""
226+
Attempts to decrypt `self.mmkv_file` data with `key` and `self.iv` using
227+
AES-128-CFB. Will return decrypted bytes as a fully decrypted MMKV file.
228+
Will pad `key` with NULL bytes or only take the first 16-bytes.
229+
230+
:param key: 16-byte AES key, or hexstring AES key
231+
:return: decrypted mmkv file in bytes
232+
"""
233+
print(f'iv: {self.iv}')
234+
if isinstance(key, str):
235+
key = bytes.fromhex(key)
236+
237+
# Validate the key size
238+
if len(key) > 16:
239+
key = key[:16]
240+
elif len(key) < 16:
241+
diff = (16 - len(key)) * b'\x00'
242+
key += diff
243+
244+
size = self.mmkv_file.read(4)
245+
print(f'size: {size}')
246+
encrypted_data = self.mmkv_file.read()
247+
248+
cipher = Cipher(algorithms.AES(key), modes.CFB(self.iv))
249+
decryptor = cipher.decryptor()
250+
res = decryptor.update(encrypted_data) + decryptor.finalize()
251+
res = size + res
252+
253+
self.mmkv_file = BytesIO(res)
254+
return res
255+
256+
257+
258+
'''
259+
Decoding Procedures
260+
'''
210261
def decode_into_map(self) -> DefaultDict[str, List[bytes]]:
211262
"""
212263
A best-effort approach on linearly parsing the `mmkv_file` stream and building up
@@ -215,8 +266,11 @@ def decode_into_map(self) -> DefaultDict[str, List[bytes]]:
215266
:return: a built up defaultdict, which is also an instance variable
216267
"""
217268

269+
# Prepare first
270+
self._prepare_mmkv_stream_for_decoding()
271+
218272
# Get size of database
219-
db_size = self.get_db_size()
273+
db_size = self._get_db_size()
220274

221275
# Check db_size - max out if needed
222276
if db_size == 0:
@@ -276,6 +330,7 @@ def decode_into_map(self) -> DefaultDict[str, List[bytes]]:
276330

277331
return self.decoded_map
278332

333+
279334
def decode_as_int32(self, value: Union[str, bytes]) -> int:
280335
"""
281336
Decodes `value` as a signed 32-bit int.
@@ -287,6 +342,7 @@ def decode_as_int32(self, value: Union[str, bytes]) -> int:
287342
value = bytes.fromhex(value)
288343
return decode_signed_varint(BytesIO(value), mask=32)[0]
289344

345+
290346
def decode_as_int64(self, value: Union[str, bytes]) -> int:
291347
"""
292348
Decodes `value` as a signed 64-bit int.
@@ -309,6 +365,7 @@ def decode_as_uint32(self, value: Union[str, bytes]) -> int:
309365
value = bytes.fromhex(value)
310366
return decode_unsigned_varint(BytesIO(value), mask=32)[0]
311367

368+
312369
def decode_as_uint64(self, value: Union[str, bytes]) -> int:
313370
"""
314371
Decodes `value` as an unsigned 64-bit int.
@@ -320,6 +377,7 @@ def decode_as_uint64(self, value: Union[str, bytes]) -> int:
320377
value = bytes.fromhex(value)
321378
return decode_unsigned_varint(BytesIO(value), mask=64)[0]
322379

380+
323381
def decode_as_string(self, value: Union[str, bytes]) -> Optional[str]:
324382
"""
325383
Attempts to decodes `value` as a UTF-8 string.
@@ -343,6 +401,7 @@ def decode_as_string(self, value: Union[str, bytes]) -> Optional[str]:
343401
print(f'[+] Could not UTF-8 decode {value!r}')
344402
return None
345403

404+
346405
def decode_as_bytes(self, value: Union[str, bytes]) -> Optional[bytes]:
347406
"""
348407
Decodes `value` as bytes.
@@ -366,6 +425,7 @@ def decode_as_bytes(self, value: Union[str, bytes]) -> Optional[bytes]:
366425
print(f'[+] Could not decode bytes')
367426
return None
368427

428+
369429
def decode_as_float(self, value: Union[str, bytes]) -> Optional[float]:
370430
"""
371431
Decodes `value` as a double (8-bytes), which is a float type in Python.
@@ -377,11 +437,12 @@ def decode_as_float(self, value: Union[str, bytes]) -> Optional[float]:
377437
value = bytes.fromhex(value)
378438

379439
if len(value) != 8:
380-
print(f'[+] Could not float decode {value} due to length')
440+
print(f'[+] Could not float decode {value!r} due to length')
381441
return None
382442

383443
return struct.unpack('<d', value)[0]
384444

445+
385446
def decode_as_bool(self, value: Union[str, bytes]) -> Optional[bool]:
386447
"""
387448
Attempts to decode `value` as a boolean.

frontend/src/Footer.svelte

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@
66
<div class="page-footer">
77
<h1>Features</h1>
88
<ul>
9-
<li>Visualize the most recent key-value pairs, as well as older logged values not available via the API.</li>
9+
<li>Visualize the most recent key-value pairs, as well as older logged values not available via the native API.</li>
1010
<li>Interpret the values in all possible ways, including strings, bytes, ints, floats, and bools.</li>
11+
<li>Decrypt MMKV files with the associated <i>.crc</i> file and an AES key</li>
12+
</ul>
13+
<h1>Credits</h1>
14+
<ul>
15+
<li><a href="https://qwtel.com/">Florian Klampfer</a>, inspiring me with <a href="https://sqliteviewer.app/"><i>SQLite Viewer</i></a></li>
16+
<li><a href="https://pyodide.org/en/stable/">Pyodide</a> for allowing me to write the parser in my favorite language</li>
1117
</ul>
12-
<!-- <p class="text">- Visualize the most recent key-value pairs, as well as older logged values not available via the API.</p>
13-
<p class="text">- Interpret the values in all possible ways, including strings, bytes, ints, floats, and bools.</p> -->
1418
<p>© Steven Pak 2022</p>
1519
</div>
1620

frontend/src/MMKVCell.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@
8787

8888
<MMKVCellModal
8989
bind:hidden={expand_hidden}
90-
data={interpretHexData(dataTypeIndex)}
91-
dataType={dataType.split('-')[0]}/>
90+
content={interpretHexData(dataTypeIndex)}
91+
subject={dataType.split('-')[0]}/>
9292

9393

9494
<!-- Styles -->

frontend/src/MMKVCellModal.svelte

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,78 @@
11
<script>
22
3+
/**
4+
* Imports
5+
*/
6+
import { createEventDispatcher } from 'svelte';
7+
8+
/**
9+
* State
10+
*/
311
export let hidden = true
4-
export let data
5-
export let dataType
12+
export let content = ''
13+
export let subject = ''
14+
let aesKey = ""
15+
const dispatch = createEventDispatcher();
616
17+
/**
18+
* Functions
19+
*/
20+
21+
// Will copy the `content` state string into the browser clipboard
722
async function copyContent(e) {
823
e.stopPropagation()
924
console.log("[+] Copy Content")
10-
navigator.clipboard.writeText(data).then(
25+
navigator.clipboard.writeText(content).then(
1126
() => {
12-
console.log('[+] Copied data')
27+
console.log('[+] Copied content')
1328
},
1429
() => {
1530
console.log('[+] Copy failed')
1631
})
1732
}
33+
34+
// Will dispatch a custom event called "sendAesKey", which will
35+
// hold the `aesKey` hexstring from the user.
36+
function sendAesKey() {
37+
console.log(`[+] AES Key: ${aesKey}`)
38+
dispatch('sendAesKey', {
39+
aesKey: aesKey
40+
})
41+
}
42+
43+
// Will reset all the props to default.
44+
function exit() {
45+
console.log('[+] Reset props from MMKVCellModal')
46+
hidden = true
47+
content = ''
48+
subject = ''
49+
}
50+
1851
</script>
1952

2053

2154
<!-- HTML -->
2255
<div class="modal" class:hidden={hidden}>
23-
<span class="data-type">{dataType}</span>
56+
<span class="subject">{subject}</span>
2457
<span class="material-icons md-18" on:click={copyContent}>content_copy</span>
2558
<hr>
26-
<span>{data}</span>
27-
</div>
59+
<span>{content}</span>
2860

29-
<div class="overlay" class:hidden={hidden} on:click={() => hidden = true}>
30-
61+
{#if subject == "Encrypted MMKV Database"}
62+
<br><br>
63+
<label for="aes-key">Please enter your AES key in the form of a hexstring:</label>
64+
<input bind:value={aesKey} type="text" id="aes-key" name="aes-key">
65+
<br>
66+
<button on:click={sendAesKey}>Decrypt</button>
67+
{/if}
3168
</div>
3269

70+
<div class="overlay" class:hidden={hidden} on:click={exit}></div>
71+
3372

3473
<!-- Styling -->
3574
<style>
75+
span { white-space: pre-line; }
3676
.modal {
3777
padding: 16px;
3878
width: 60vw;
@@ -61,5 +101,5 @@
61101
.hidden {
62102
display: none;
63103
}
64-
.data-type {color: rgba(0, 0, 0, 0.5);}
104+
.subject {color: rgba(0, 0, 0, 0.5);}
65105
</style>

0 commit comments

Comments
 (0)