Mon, 01 Aug 2022 11:29:27 +0100
Initial commit
0 | 1 | #!/usr/bin/env lua5.3 |
2 | ||
3 | if not _VERSION:match("^Lua 5%.[34]") then | |
4 | print("This utility requires Lua 5.3/5.4"); | |
5 | return 1; | |
6 | end | |
7 | ||
8 | local have_ciphers, ciphers = pcall(require, "openssl.cipher"); | |
9 | if not have_ciphers then | |
10 | print("openssl.ciphers module not found."); | |
11 | print("On Debian, install lua-luaossl, or install luarocks and run"); | |
12 | print(" luarocks install luaossl"); | |
13 | print(""); | |
14 | end | |
15 | ||
16 | local have_kdf, kdf = pcall(require, "openssl.kdf"); | |
17 | if not have_kdf then | |
18 | print("openssl.kdf module not found."); | |
19 | print("On Debian, install lua-luaossl, or install luarocks and run"); | |
20 | print(" luarocks install luaossl"); | |
21 | print(""); | |
22 | end | |
23 | ||
24 | local have_zlib, zlib = pcall(require, "zlib"); | |
25 | if not have_zlib then | |
26 | print("zlib module not found."); | |
27 | print("On Debian, install lua-zlib, or install luarocks and run"); | |
28 | print(" luarocks install lua-zlib"); | |
29 | print(""); | |
30 | end | |
31 | ||
32 | if #arg == 0 then | |
33 | print("Fix Conversations backup files that contain NUL bytes"); | |
34 | print(""); | |
35 | print("Usage:"); | |
36 | print(""); | |
37 | print("", arg[0].." INPUT_FILE PASSWORD"); | |
38 | print(""); | |
39 | print("On success, a new backup file will be created with the suffix '-fixed.ceb'."); | |
40 | print(""); | |
41 | end | |
42 | ||
43 | if #arg == 0 or not have_zlib or not have_kdf or not have_ciphers then | |
44 | return 1; | |
45 | end | |
46 | ||
47 | local input_filename = assert(arg[1], "no ceb file specified"); | |
48 | ||
49 | local file_password = assert(arg[2], "no password specified"); | |
50 | ||
51 | local function read_header(f) | |
52 | local function read_int() | |
53 | return (">i4"):unpack(f:read(4)); | |
54 | end | |
55 | local function read_short() | |
56 | return (">i2"):unpack(f:read(2)); | |
57 | end | |
58 | local function read_long() | |
59 | return (">i8"):unpack(f:read(8)); | |
60 | end | |
61 | local function read_string() | |
62 | local n = read_short(); | |
63 | return f:read(n); | |
64 | end | |
65 | ||
66 | return { | |
67 | version = read_int(); | |
68 | app_id = read_string(); | |
69 | jid = read_string(); | |
70 | timestamp = math.floor(read_long()/1000); | |
71 | iv = f:read(12); | |
72 | salt = f:read(16); | |
73 | }; | |
74 | end | |
75 | ||
76 | local function write_header(f, header) | |
77 | local function write_int(f, n) | |
78 | f:write((">i4"):pack(n)); | |
79 | end | |
80 | local function write_short(f, n) | |
81 | f:write((">i2"):pack(n)); | |
82 | end | |
83 | local function write_long(f, n) | |
84 | f:write((">i8"):pack(n)); | |
85 | end | |
86 | local function write_string(f, s) | |
87 | write_short(f, #s); | |
88 | f:write(s); | |
89 | end | |
90 | ||
91 | write_int(f, header.version); | |
92 | write_string(f, header.app_id); | |
93 | write_string(f, header.jid); | |
94 | write_long(f, header.timestamp*1000); | |
95 | assert(#header.iv == 12); | |
96 | f:write(header.iv); | |
97 | assert(#header.salt == 16); | |
98 | f:write(header.salt); | |
99 | end | |
100 | ||
101 | local f = io.open(input_filename); | |
102 | ||
103 | local header = read_header(f); | |
104 | ||
105 | print("version", header.version); | |
106 | print("app", header.app_id); | |
107 | print("jid", header.jid); | |
108 | print("timestamp", os.date("%c", header.timestamp)); | |
109 | ||
110 | ||
111 | local function generate_key(password, salt) | |
112 | return kdf.derive({ | |
113 | type = "PBKDF2"; | |
114 | md = "sha1"; | |
115 | pass = password; | |
116 | salt = salt; | |
117 | iter = 1024; | |
118 | outlen = 128/8; | |
119 | }); | |
120 | end | |
121 | ||
122 | print("k", #(generate_key(file_password, header.salt))); | |
123 | ||
124 | local decryption_key = generate_key(file_password, header.salt); | |
125 | ||
126 | local cipher = ciphers.new("AES-128-GCM"):decrypt(decryption_key, header.iv); | |
127 | ||
128 | local decompress = zlib.inflate(); | |
129 | ||
130 | local compress = zlib.deflate(); | |
131 | local cipher_out = ciphers.new("AES-128-GCM"):encrypt(decryption_key, header.iv); | |
132 | ||
133 | local null_replacement = string.char(0xE2, 0x90, 0x80); -- U+2400 SYMBOL FOR NULL | |
134 | ||
135 | local outfile = assert(io.open(input_filename:gsub("%.ceb$", "-fixed.ceb"), "w+")); | |
136 | write_header(outfile, header); | |
137 | ||
138 | repeat | |
139 | local enc_data = f:read(4096); | |
140 | if not enc_data then break; end | |
141 | ||
142 | local gz_data = assert(cipher:update(enc_data)); | |
143 | ||
144 | local status, data, eof = pcall(decompress, gz_data); | |
145 | if not status then | |
146 | print("EE: Failed to decompress: "..tostring(data)); | |
147 | return 1; | |
148 | end | |
149 | ||
150 | local status, recompressed = pcall(compress, ((data:gsub("\000", null_replacement)))); | |
151 | if not status then | |
152 | print("EE: Failed to compress: "..tostring(data)); | |
153 | return 1; | |
154 | end | |
155 | local reencrypted = cipher_out:update(recompressed); | |
156 | outfile:write(reencrypted); | |
157 | until eof | |
158 | ||
159 | local final_data = cipher_out:final(); | |
160 | if final_data and final_data ~= "" then | |
161 | outfile:write(final_data); | |
162 | end | |
163 | outfile:close(); | |
164 | ||
165 | print("Done"); |