Mon, 01 Aug 2022 11:29:27 +0100
Initial commit
#!/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");