fix-ceb-nulls.lua

changeset 0
ed346ec34e2a
equal deleted inserted replaced
-1:000000000000 0:ed346ec34e2a
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");

mercurial