Skip to content

Commit

Permalink
KeePass export: add support for Pleasant Password's TOTP-data schema …
Browse files Browse the repository at this point in the history
…(KC-786)
  • Loading branch information
aaunario-keeper authored and sk-keeper committed Jun 28, 2024
1 parent fe9c026 commit 925484c
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 1 deletion.
7 changes: 7 additions & 0 deletions keepercommander/importer/keepass/keepass.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
Record, Folder, SharedFolder, BytesAttachment, RecordField
from ... import utils
from ...display import bcolors
from ...utils import parse_totp_uri

_REFERENCE = r'\{REF:([TUPAN])@([IT]):([^\}]+)\}'

Expand Down Expand Up @@ -326,6 +327,12 @@ def do_export(self, filename, records, file_password=None, **kwargs):
for cf in r.fields:
if cf.type == 'oneTimeCode':
entry.otp = cf.value
# Set custom fields for Pleasant Password TOTP compatibility
totp_props = parse_totp_uri(cf.value)
for key in ['secret', 'period', 'issuer', 'digits']:
val = totp_props.get(key)
val and entry.set_custom_property(f'TOTP{key.capitalize()}', str(val))

continue
if cf.type and cf.label:
title = f'${cf.type}:{cf.label}'
Expand Down
40 changes: 39 additions & 1 deletion keepercommander/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import math
import re
import time
from urllib.parse import urlparse
from urllib.parse import urlparse, parse_qs, unquote

from . import crypto
from .constants import EMAIL_PATTERN
Expand Down Expand Up @@ -320,3 +320,41 @@ def size_to_str(size): # type: (int) -> str
return f'{size:.2f} Mb'
size = size / 1024
return f'{size:,.2f} Gb'

def parse_totp_uri(uri): # type: (str) -> Dict[str, Union[str, int, None]]
def parse_int(val):
return val and int(val)

def decode_uri_component(component): # type: (str) -> str
return unquote(component or '').strip()

result = dict()

if not uri:
return result

parsed = urlparse(uri)
if parsed.scheme == 'otpauth':
label = re.sub(r'^/+', '', parsed.path or '')
parts = re.split(r':|%3A', label)
parts = [part for part in parts if part]
account_name = len(parts) and parts.pop()
issuer = len(parts) and parts.pop()

parsed = parse_qs(parsed.query)

issuers = parsed.get('issuer')
secrets = parsed.get('secret')
algorithms = parsed.get('algorithm')
digits_vals = parsed.get('digits')
periods = parsed.get('period')
result = {
'issuer': decode_uri_component(issuers and next(iter(issuers)) or issuer),
'account': decode_uri_component(account_name),
'secret': secrets and next(iter(secrets)),
'algorithm': algorithms and next(iter(algorithms)) or 'SHA1',
'digits': parse_int(digits_vals and next(iter(digits_vals))) or 6,
'period': parse_int(periods and next(iter(periods))) or 30
}

return result

0 comments on commit 925484c

Please sign in to comment.