fix-ceb-nulls.lua

changeset 0
ed346ec34e2a
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/fix-ceb-nulls.lua	Mon Aug 01 11:29:27 2022 +0100
@@ -0,0 +1,165 @@
+#!/usr/bin/env lua5.3
+
+if not _VERSION:match("^Lua 5%.[34]") then
+	print("This utility requires Lua 5.3/5.4");
+	return 1;
+end
+
+local have_ciphers, ciphers = pcall(require, "openssl.cipher");
+if not have_ciphers then
+	print("openssl.ciphers module not found.");
+	print("On Debian, install lua-luaossl, or install luarocks and run");
+	print("  luarocks install luaossl");
+	print("");
+end
+
+local have_kdf, kdf = pcall(require, "openssl.kdf");
+if not have_kdf then
+	print("openssl.kdf module not found.");
+	print("On Debian, install lua-luaossl, or install luarocks and run");
+	print("  luarocks install luaossl");
+	print("");
+end
+
+local have_zlib, zlib = pcall(require, "zlib");
+if not have_zlib then
+	print("zlib module not found.");
+	print("On Debian, install lua-zlib, or install luarocks and run");
+	print("  luarocks install lua-zlib");
+	print("");
+end
+
+if #arg == 0 then
+	print("Fix Conversations backup files that contain NUL bytes");
+	print("");
+	print("Usage:");
+	print("");
+	print("", arg[0].." INPUT_FILE PASSWORD");
+	print("");
+	print("On success, a new backup file will be created with the suffix '-fixed.ceb'.");
+	print("");
+end
+
+if #arg == 0 or not have_zlib or not have_kdf or not have_ciphers then
+	return 1;
+end
+
+local input_filename = assert(arg[1], "no ceb file specified");
+
+local file_password = assert(arg[2], "no password specified");
+
+local function read_header(f)
+	local function read_int()
+		return (">i4"):unpack(f:read(4));
+	end
+	local function read_short()
+		return (">i2"):unpack(f:read(2));
+	end
+	local function read_long()
+		return (">i8"):unpack(f:read(8));
+	end
+	local function read_string()
+		local n = read_short();
+		return f:read(n);
+	end
+
+	return {
+		version = read_int();
+		app_id = read_string();
+		jid = read_string();
+		timestamp = math.floor(read_long()/1000);
+		iv = f:read(12);
+		salt = f:read(16);
+	};
+end
+
+local function write_header(f, header)
+	local function write_int(f, n)
+		f:write((">i4"):pack(n));
+	end
+	local function write_short(f, n)
+		f:write((">i2"):pack(n));
+	end
+	local function write_long(f, n)
+		f:write((">i8"):pack(n));
+	end
+	local function write_string(f, s)
+		write_short(f, #s);
+		f:write(s);
+	end
+
+	write_int(f, header.version);
+	write_string(f, header.app_id);
+	write_string(f, header.jid);
+	write_long(f, header.timestamp*1000);
+	assert(#header.iv == 12);
+	f:write(header.iv);
+	assert(#header.salt == 16);
+	f:write(header.salt);
+end
+
+local f = io.open(input_filename);
+
+local header = read_header(f);
+
+print("version", header.version);
+print("app", header.app_id);
+print("jid", header.jid);
+print("timestamp", os.date("%c", header.timestamp));
+
+
+local function generate_key(password, salt)
+	return kdf.derive({
+		type = "PBKDF2";
+		md = "sha1";
+		pass = password;
+		salt = salt;
+		iter = 1024;
+		outlen = 128/8;
+	});
+end
+
+print("k", #(generate_key(file_password, header.salt)));
+
+local decryption_key = generate_key(file_password, header.salt);
+
+local cipher = ciphers.new("AES-128-GCM"):decrypt(decryption_key, header.iv);
+
+local decompress = zlib.inflate();
+
+local compress = zlib.deflate();
+local cipher_out = ciphers.new("AES-128-GCM"):encrypt(decryption_key, header.iv);
+
+local null_replacement = string.char(0xE2, 0x90, 0x80); -- U+2400 SYMBOL FOR NULL
+
+local outfile = assert(io.open(input_filename:gsub("%.ceb$", "-fixed.ceb"), "w+"));
+write_header(outfile, header);
+
+repeat
+	local enc_data = f:read(4096);
+	if not enc_data then break; end
+
+	local gz_data = assert(cipher:update(enc_data));
+
+	local status, data, eof = pcall(decompress, gz_data);
+	if not status then
+		print("EE: Failed to decompress: "..tostring(data));
+		return 1;
+	end
+
+	local status, recompressed = pcall(compress, ((data:gsub("\000", null_replacement))));
+	if not status then
+		print("EE: Failed to compress: "..tostring(data));
+		return 1;
+	end
+	local reencrypted = cipher_out:update(recompressed);
+	outfile:write(reencrypted);
+until eof
+
+local final_data = cipher_out:final();
+if final_data and final_data ~= "" then
+	outfile:write(final_data);
+end
+outfile:close();
+
+print("Done");

mercurial