Separate class for properties to handle protected and web-fields.

This commit is contained in:
Markus Birth 2021-09-27 01:41:49 +02:00
parent d54ff5eb75
commit 557c5c4f7d
Signed by: mbirth
GPG Key ID: A9928D7A098C3A9A
5 changed files with 177 additions and 93 deletions

View File

@ -39,7 +39,6 @@ uuid_map = {}
for item in opif: for item in opif:
props = item.get_all_props()
# Fields that are not to be added as custom properties # Fields that are not to be added as custom properties
fids_done = ["passwordHistory"] fids_done = ["passwordHistory"]
@ -52,11 +51,12 @@ for item in opif:
target_group_name = "Recycle Bin" target_group_name = "Recycle Bin"
# Add entry to KeePass # Add entry to KeePass
entry = kp.add_entry(target_group_name, props["title"]) entry = kp.add_entry(target_group_name, item.get_property("title").value)
fids_done.append("title") fids_done.append("title")
# UUID - memorise for later linking? # UUID - memorise for later linking (if supported by output format)?
uuid_map[props["uuid"]] = entry.uuid uuid = item.get_property("uuid").value
uuid_map[uuid] = entry.uuid
fids_done.append("uuid") fids_done.append("uuid")
# Icon # Icon
@ -64,16 +64,19 @@ for item in opif:
kp.set_icon(kp_icon) kp.set_icon(kp_icon)
# URLs # URLs
if "location" in props: location = item.get_property("location")
kp.add_url(props["location"]) if location:
kp.add_url(location.value)
fids_done.append("location") fids_done.append("location")
fids_done.append("locationKey") fids_done.append("locationKey")
if "URLs" in props: urls = item.get_property("URLs")
for u in props["URLs"]: if urls:
for u in urls.raw:
kp.add_url(u["url"]) kp.add_url(u["url"])
fids_done.append("URLs") fids_done.append("URLs")
if "URL" in props: url = item.get_property("URL")
kp.add_url(props["URL"]) if url:
kp.add_url(url.value)
fids_done.append("URL") fids_done.append("URL")
# Tags # Tags
@ -87,13 +90,15 @@ for item in opif:
kp.add_totp(totp[0], title=totp[1]) kp.add_totp(totp[0], title=totp[1])
# Notes # Notes
if "notesPlain" in props: notes_plain = item.get_property("notesPlain")
entry.notes = props["notesPlain"] print("Notes: {}".format(repr(notes_plain)))
if notes_plain:
entry.notes = notes_plain.raw
fids_done.append("notesPlain") fids_done.append("notesPlain")
# Dates # Dates
entry.ctime = datetime.datetime.fromtimestamp(props["createdAt"]) entry.ctime = datetime.datetime.fromtimestamp(item.get_property("createdAt").raw)
entry.mtime = datetime.datetime.fromtimestamp(props["updatedAt"]) entry.mtime = datetime.datetime.fromtimestamp(item.get_property("updatedAt").raw)
fids_done.append("createdAt") fids_done.append("createdAt")
fids_done.append("updatedAt") fids_done.append("updatedAt")
@ -105,41 +110,42 @@ for item in opif:
if type(seek_fields) is str: if type(seek_fields) is str:
seek_fields = [seek_fields] seek_fields = [seek_fields]
for fid in seek_fields: for fid in seek_fields:
if fid in props: prop = item.get_property(fid)
setattr(entry, map_field, props[fid]) if prop:
setattr(entry, map_field, prop.value)
fids_done.append(fid) fids_done.append(fid)
break break
# Set remaining properties # Set remaining properties
for k, v in props.items(): for k in item.get_property_keys():
if k in ["Password"]: if k in ["Password"]:
# Forbidden name # Forbidden name
continue continue
if k[:5] == "TOTP_":
# Skip OTPs as they're handled separately
continue
if k in RECORD_MAP["General"]["ignored"]: if k in RECORD_MAP["General"]["ignored"]:
# Skip ignored fields # Skip ignored fields
continue continue
if k in fids_done: if k in fids_done:
# Skip fields processed elsewhere # Skip fields processed elsewhere
continue continue
kp.set_prop(k, str(v)) v = item.get_property(k)
if v.is_web_field:
kp.set_prop("KPH: {}".format(v.title), v.value, protected=v.is_protected)
else:
kp.set_prop(v.title, v.value, protected=v.is_protected)
# TODO: scope: Never = never suggest in browser (i.e. don't add KPH fields) # TODO: scope: Never = never suggest in browser (i.e. don't add KPH fields)
secure = item.raw["secureContents"]
# Other web fields
if "fields" in secure:
for field in secure["fields"]:
d = field.get("designation")
if d != "username" and d != "password":
entry.set_custom_property("KPH: {}".format(field["name"]), field["value"])
# AFTER ALL OTHER PROCESSING IS DONE: Password history # AFTER ALL OTHER PROCESSING IS DONE: Password history
if "passwordHistory" in props: password_history = item.get_property("passwordHistory")
if password_history:
original_password = entry.password original_password = entry.password
original_mtime = entry.mtime original_mtime = entry.mtime
for p in props["passwordHistory"]: for p in password_history.raw:
d = datetime.datetime.fromtimestamp(p["time"]) d = datetime.datetime.fromtimestamp(p["time"])
entry.mtime = d entry.mtime = d
entry.password = p["value"] entry.password = p["value"]

View File

@ -51,7 +51,7 @@ class KpWriter:
suffix = "_{}".format(suffix_ctr) suffix = "_{}".format(suffix_ctr)
self.set_prop("TimeOtp-Secret-Base32{}".format(suffix), init_string, True) self.set_prop("TimeOtp-Secret-Base32{}".format(suffix), init_string, True)
self.set_prop("otp{}".format(suffix), otp_url) self.set_prop("otp{}".format(suffix), otp_url, True)
if len(title) > 0: if len(title) > 0:
self.set_prop("otp_title{}".format(suffix), title) self.set_prop("otp_title{}".format(suffix), title)

View File

@ -5,6 +5,7 @@ General:
- securityLevel - securityLevel
- typeName - typeName
- uuid - uuid
- trashed
# Mapping of 1P types for best conversion # Mapping of 1P types for best conversion

View File

@ -1,5 +1,5 @@
import sys import sys
from datetime import datetime from . import OnepifEntryProperty as oep
TYPES = { TYPES = {
"112": "API Credential", "112": "API Credential",
@ -24,12 +24,16 @@ TYPES = {
"wallet.computer.Router": "Wireless Router", "wallet.computer.Router": "Wireless Router",
} }
ANSI_RED = u"\u001b[1;31m"
ANSI_RESET = u"\u001b[0m"
class OnepifEntry(): class OnepifEntry():
def __init__(self, data): def __init__(self, data):
self.raw = data self.raw = data
self.set_type(data["typeName"]) self.set_type(data["typeName"])
self.properties = []
self.parse()
def set_type(self, new_type: str): def set_type(self, new_type: str):
if new_type not in TYPES: if new_type not in TYPES:
@ -65,6 +69,27 @@ class OnepifEntry():
return self.raw["trashed"] return self.raw["trashed"]
return False return False
def add_property(self, property: oep.OnepifEntryProperty):
self.properties.append(property)
def get_property_keys(self):
keys = []
for p in self.properties:
keys.append(p.name)
keys = list(set(keys))
return keys
def get_property(self, key: str):
props = []
for p in self.properties:
if p.name == key:
props.append(p)
if not props:
return None
elif len(props) > 1:
print("{}Warning: Multiple properties matching '{}' found: {}. Ignoring all but the first.{}".format(ANSI_RED, key, repr(props), ANSI_RESET))
return props[0]
def add_with_unique_key(self, prop_dict: dict, new_key: str, new_value): def add_with_unique_key(self, prop_dict: dict, new_key: str, new_value):
suffix_ctr = 0 suffix_ctr = 0
tmp_key = new_key tmp_key = new_key
@ -76,72 +101,29 @@ class OnepifEntry():
new_value = new_value.replace("\x10", "") new_value = new_value.replace("\x10", "")
prop_dict[tmp_key] = new_value prop_dict[tmp_key] = new_value
def convert_section_field_to_string(self, field_data: dict) -> str: def parse_section(self, section: dict):
kind = field_data["k"]
if kind in ["string", "concealed", "email", "phone", "URL", "menu", "cctype"]:
return field_data["v"]
elif kind == "date":
return datetime.fromtimestamp(field_data["v"]).strftime("%Y-%m-%d")
elif kind == "monthYear":
month = field_data["v"] % 100
month_name = datetime.strptime(str(month), "%m").strftime("%b")
year = field_data["v"] // 100
return "{} {}".format(month_name, year)
elif kind == "address":
addr = field_data["v"]
result = ""
if addr["street"]:
result += addr["street"] + "\n"
if addr["city"]:
result += addr["city"] + "\n"
if addr["zip"]:
result += addr["zip"] + "\n"
if addr["state"]:
result += addr["state"] + "\n"
if addr["region"]:
result += addr["region"] + "\n"
if addr["country"]:
result += addr["country"].upper()
return result.strip()
elif kind == "reference":
print("WARNING: Links between items are not supported (entry: {} -> {}).".format(self.raw["title"], field_data["t"]), file=sys.stderr)
return field_data["t"]
raise Exception("Unknown data kind in section fields: {}".format(kind))
return field_data["v"]
def parse_section_into_dict(self, target_dict: dict, section: dict):
sect_title = section["title"] sect_title = section["title"]
for f in section["fields"]: for f in section["fields"]:
if "v" not in f: if "v" not in f:
# Skip fields without data # Skip fields without data
continue continue
propname = "{}: {}".format(sect_title, f["t"].title()) prop = oep.OnepifEntryProperty.from_sectionfield(f, sect_title)
if not sect_title: self.add_property(prop)
propname = f["t"]
propval = self.convert_section_field_to_string(f)
self.add_with_unique_key(target_dict, propname, propval)
def parse_fields_into_dict(self, target_dict: dict, fields: list): def parse_fields(self, fields: list):
for f in fields: for f in fields:
if f["type"] in ["C", "R"]: prop = oep.OnepifEntryProperty.from_webfield(f)
# Skip unsupported fields if prop:
print("Ignoring checkbox/radiobuttons value in entry {}.".format(self.raw["title"]), file=sys.stderr) self.add_property(prop)
continue
if "value" not in f:
# Skip fields without data
continue
if "designation" in f:
propname = f["designation"]
else:
propname = f["name"]
propval = f["value"]
if f["type"] not in ["T", "P", "E"]:
raise Exception("Unknown field type discovered: {}".format(f["type"]))
self.add_with_unique_key(target_dict, propname, propval)
def get_all_props(self) -> dict: def add_simple_prop(self, key: str, value):
props = {} if value == "\x10":
# this seems to be an "empty" indicator, so skip this
return False
prop = oep.OnepifEntryProperty(key, value)
self.add_property(prop)
def parse(self):
for k, v in self.raw.items(): for k, v in self.raw.items():
if k in ["openContents", "secureContents"]: if k in ["openContents", "secureContents"]:
# handle open/secure groups of properties # handle open/secure groups of properties
@ -157,16 +139,15 @@ class OnepifEntry():
if "fields" not in s: if "fields" not in s:
# Skip empty sections # Skip empty sections
continue continue
self.parse_section_into_dict(props, s) self.parse_section(s)
continue continue
elif k2 == "fields": elif k2 == "fields":
# For some reason this differs from the "fields" in a section # For some reason this differs from the "fields" in a section
self.parse_fields_into_dict(props, v2) self.parse_fields(v2)
continue continue
# Handle all other values (most probably string or int) # Handle all other values (most probably string or int)
self.add_with_unique_key(props, k2, v2) self.add_simple_prop(k2, v2)
continue continue
# Handle all other values # Handle all other values
self.add_with_unique_key(props, k, v) self.add_simple_prop(k, v)
# TODO: Maybe walk all keys and see if there's (xxx_dd), xxx_mm, xxx_yy and turn them into a date # TODO: Maybe walk all keys and see if there's (xxx_dd), xxx_mm, xxx_yy and turn them into a date
return props

View File

@ -0,0 +1,96 @@
import sys
from datetime import datetime
class OnepifEntryProperty():
def __init__(self, name, raw_value):
self.name = name # internal name
self.title = name # user visible name
self.raw = raw_value
self.set_value(raw_value)
self.section = None
self.type = ""
self.web_field_name = None # designation
self.is_web_field = False # has web_field_name
self.is_protected = False
def __repr__(self):
return "<OnepifEntryProperty \"{}\" ({}) = {}>".format(self.name, self.title, repr(self.raw))
def set_value(self, new_value):
if new_value == "\x10":
self.value = ""
else:
self.value = str(new_value)
@classmethod
def from_sectionfield(cls, field_dict: dict, sect_title: str = None):
key = field_dict["t"]
if not key:
key = field_dict["n"]
p = cls(key, field_dict)
if sect_title:
p.section = sect_title
p.title = "{}: {}".format(sect_title, field_dict["t"].title())
p.name = "{}_{}".format(sect_title.lower(), field_dict["t"].lower())
kind = field_dict["k"]
if kind in ["string", "email", "phone", "URL", "menu", "cctype"]:
p.set_value(field_dict["v"])
elif kind == "concealed":
p.set_value(field_dict["v"])
p.is_protected = True
elif kind == "date":
p.set_value(datetime.fromtimestamp(field_dict["v"]).strftime("%Y-%m-%d"))
elif kind == "monthYear":
month = field_dict["v"] % 100
month_name = datetime.strptime(str(month), "%m").strftime("%b")
year = field_dict["v"] // 100
p.set_value("{} {}".format(month_name, year))
elif kind == "address":
addr = field_dict["v"]
result = ""
if addr["street"]:
result += addr["street"] + "\n"
if addr["city"]:
result += addr["city"] + "\n"
if addr["zip"]:
result += addr["zip"] + "\n"
if addr["state"]:
result += addr["state"] + "\n"
if addr["region"]:
result += addr["region"] + "\n"
if addr["country"]:
result += addr["country"].upper()
p.set_value(result.strip())
elif kind == "reference":
print("WARNING: Links between items are not supported (-> {}).".format(field_dict["t"]), file=sys.stderr)
p.set_value(field_dict["t"])
else:
raise Exception("Unknown data kind in section fields: {}".format(kind))
return p
@classmethod
def from_webfield(cls, field_dict: dict):
if field_dict["type"] in ["C", "R"]:
# Skip unsupported fields
print("WARNING: Ignoring checkbox/radiobuttons value in entry.".format(), file=sys.stderr)
return None
if "value" not in field_dict or not field_dict["value"]:
# Skip fields without data
return None
if "designation" in field_dict and field_dict["designation"]:
key = field_dict["designation"]
else:
key = field_dict["name"]
p = cls(key, field_dict)
p.is_web_field = True
if "id" in field_dict:
p.web_field_name = field_dict["id"]
p.set_value(field_dict["value"])
if field_dict["type"] not in ["T", "P", "E"]:
raise Exception("Unknown field type discovered: {}".format(field_dict["type"]))
return p