import pathlib
import sys
from ccl_chromium_reader import ccl_chromium_indexeddb as idb
_KIND = {
"conversations": "conversation",
"events": "event",
"users": "user",
"clients": "client",
}
_CIPHER_MARKERS = ("cipher_text", "cipherText", "otr", "encrypted")
def _hex(s: str) -> str:
return s.encode("utf-8").hex()
def _scalar_text(x):
if isinstance(x, bool):
return "true" if x else "false"
if isinstance(x, str):
return x
if isinstance(x, int):
return str(x)
if isinstance(x, float):
return str(int(x)) if x.is_integer() else repr(x)
return None
def _message_text(v):
if isinstance(v, dict):
data = v.get("data")
if isinstance(data, dict):
for f in ("content", "text"):
if f in data:
t = _scalar_text(data[f])
if t is not None:
return t
for f in ("content", "text"):
if f in v:
t = _scalar_text(v[f])
if t is not None:
return t
return None
def _is_encrypted(v):
if isinstance(v, (bytes, bytearray)):
return True
if not isinstance(v, dict):
return False
data = v.get("data")
if isinstance(data, (bytes, bytearray)):
return True
for m in _CIPHER_MARKERS:
if m in v:
return True
if isinstance(data, dict) and m in data:
return True
return False
def _key_text(key) -> str:
val = getattr(key, "value", key)
if isinstance(val, str):
return val
if isinstance(val, (bytes, bytearray)):
return "0x" + val.hex()
return str(val)
def _fields_for(kind, v):
out = []
if not isinstance(v, dict):
return out
if kind == "conversation":
cid = _scalar_text(v.get("id"))
if cid is not None:
out.append(("id", cid))
out.append(("conversation", cid)) name = _scalar_text(v.get("name"))
if name is not None:
out.append(("name", name))
elif kind == "event":
eid = _scalar_text(v.get("id"))
if eid is not None:
out.append(("id", eid))
conv = _scalar_text(v.get("conversation"))
if conv is not None:
out.append(("conversation", conv))
sender = v.get("from", v.get("sender"))
sender = _scalar_text(sender)
if sender is not None:
out.append(("sender", sender))
time = _scalar_text(v.get("time"))
if time is not None:
out.append(("time", time))
mt = _scalar_text(v.get("type"))
if mt is not None:
out.append(("message_type", mt))
text = _message_text(v)
if text is not None:
out.append(("text", text))
elif kind == "user":
uid = _scalar_text(v.get("id"))
if uid is not None:
out.append(("id", uid))
name = _scalar_text(v.get("name"))
if name is not None:
out.append(("name", name))
elif kind == "client":
cid = _scalar_text(v.get("id"))
if cid is not None:
out.append(("id", cid))
name = v.get("model", v.get("class", v.get("label")))
name = _scalar_text(name)
if name is not None:
out.append(("name", name))
return out
def main(argv):
if len(argv) != 2:
sys.stderr.write("usage: ccl_oracle.py <indexeddb-leveldb-dir>\n")
return 2
store_dir = pathlib.Path(argv[1])
if not store_dir.is_dir():
sys.stderr.write(f"not a directory: {store_dir}\n")
return 2
wdb = idb.WrappedIndexDB(store_dir)
out = []
for dbid in wdb.database_ids:
db = wdb[dbid.dbid_no]
for store_name in db.object_store_names:
store = db.get_object_store_by_name(store_name)
kind = _KIND.get(store_name, "unknown")
for rec in store.iterate_records(live_only=True):
key = _key_text(rec.key)
out.append(f"REC\t{_hex(store_name)}\t{_hex(key)}\t{_hex(kind)}")
for fname, fval in _fields_for(kind, rec.value):
out.append(
f"FIELD\t{_hex(store_name)}\t{_hex(key)}\t{_hex(fname)}\t{_hex(fval)}"
)
if kind == "event" and _is_encrypted(rec.value):
out.append(f"ENC\t{_hex(store_name)}\t{_hex(key)}")
sys.stdout.write("\n".join(out))
if out:
sys.stdout.write("\n")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))