|
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"); |