-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added tests and more general functionality
- Loading branch information
1 parent
6680b3a
commit be86df5
Showing
13 changed files
with
363 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,114 @@ | ||
import json | ||
import sys | ||
import typing | ||
from collections import defaultdict | ||
from pathlib import Path | ||
from typing import Optional | ||
|
||
from ._security import decrypt, retrieve_credentials, save_credentials | ||
from ._types import TwoFactorAuthDetails | ||
from ._security import decrypt, keyring_manager | ||
from ._types import TwoFactorAuthDetails, into_class | ||
from .utils import flatten, fuzzy_match | ||
|
||
T_TwoFactorAuthDetails = typing.TypeVar("T_TwoFactorAuthDetails", bound=TwoFactorAuthDetails) | ||
|
||
def load_services(filename: str) -> list[TwoFactorAuthDetails]: | ||
|
||
class TwoFactorStorage(typing.Generic[T_TwoFactorAuthDetails]): | ||
_multidict: defaultdict[str, list[T_TwoFactorAuthDetails]] | ||
count: int | ||
|
||
def __init__(self, _klass: typing.Type[T_TwoFactorAuthDetails] = None) -> None: | ||
# _klass is purely for annotation atm | ||
|
||
self._multidict = defaultdict(list) # one name can map to multiple keys | ||
self.count = 0 | ||
|
||
def __bool__(self) -> bool: | ||
return self.count > 0 | ||
|
||
def add(self, entries: list[T_TwoFactorAuthDetails]) -> None: | ||
for entry in entries: | ||
self._multidict[entry.name].append(entry) | ||
|
||
self.count += len(entries) | ||
|
||
def __getitem__(self, item: str) -> list[T_TwoFactorAuthDetails]: | ||
# class[property] syntax | ||
return self._multidict[item] | ||
|
||
def keys(self) -> list[str]: | ||
return list(self._multidict.keys()) | ||
|
||
def _fuzzy_find(self, find: typing.Optional[str], fuzz_threshold: int) -> list[T_TwoFactorAuthDetails]: | ||
if find is None: | ||
# don't loop | ||
return list(self) | ||
|
||
all_items = self._multidict.items() | ||
|
||
find = find.upper() | ||
# if nothing found exactly, try again but fuzzy (could be slower) | ||
# search in key: | ||
fuzzy = [ | ||
# search in key | ||
v | ||
for k, v in all_items | ||
if fuzzy_match(k.upper(), find) > fuzz_threshold | ||
] | ||
if fuzzy and (flat := flatten(fuzzy)): | ||
return flat | ||
|
||
# search in value: | ||
return [ | ||
# search in value instead | ||
v | ||
for v in list(self) | ||
if fuzzy_match(str(v).upper(), find) > fuzz_threshold | ||
] | ||
|
||
def find(self, target: Optional[str] = None, fuzz_threshold: int = 75) -> list[T_TwoFactorAuthDetails]: | ||
# first try exact match: | ||
if items := self._multidict.get(target or ""): | ||
return items | ||
# else: fuzzy match: | ||
return self._fuzzy_find(target, fuzz_threshold) | ||
|
||
def all(self) -> list[T_TwoFactorAuthDetails]: | ||
return list(self) | ||
|
||
def __iter__(self) -> typing.Generator[T_TwoFactorAuthDetails, None, None]: | ||
for entries in self._multidict.values(): | ||
yield from entries | ||
|
||
def __repr__(self) -> str: | ||
return f"<TwoFactorStorage with {len(self._multidict)} keys and {self.count} entries>" | ||
|
||
|
||
def load_services(filename: str, _max_retries: int = 0) -> TwoFactorStorage[TwoFactorAuthDetails]: | ||
filepath = Path(filename) | ||
with filepath.open() as f: | ||
data_raw = f.read() | ||
data = json.loads(data_raw) | ||
|
||
storage = TwoFactorStorage(TwoFactorAuthDetails) | ||
|
||
if decrypted := data["services"]: | ||
services = into_class(decrypted, TwoFactorAuthDetails) | ||
storage.add(services) | ||
return storage | ||
|
||
encrypted = data["servicesEncrypted"] | ||
password = retrieve_credentials(filename) or save_credentials(filename) | ||
|
||
return decrypt(encrypted, password) | ||
retries = 0 | ||
while True: | ||
password = keyring_manager.retrieve_credentials(filename) or keyring_manager.save_credentials(filename) | ||
try: | ||
entries = decrypt(encrypted, password) | ||
storage.add(entries) | ||
return storage | ||
except PermissionError as e: | ||
retries += 1 # only really useful for pytest | ||
print(e, file=sys.stderr) | ||
keyring_manager.delete_credentials(filename) | ||
|
||
if _max_retries and retries > _max_retries: | ||
raise e |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,24 @@ | ||
import sys | ||
|
||
from ._security import cleanup_keyring | ||
from ._security import keyring_manager | ||
from .core import load_services | ||
|
||
|
||
def _main(filename: str = '') -> None: | ||
cleanup_keyring() | ||
def _main(filename: str = "") -> None: | ||
keyring_manager.cleanup_keyring() | ||
|
||
if not filename: | ||
print('todo: remember previous file(s)') | ||
print("todo: remember previous file(s)") | ||
return | ||
|
||
decrypted = load_services(filename) | ||
|
||
print([(_, _.generate()) for _ in decrypted]) | ||
print([(_, _.generate()) for _ in decrypted.all()]) | ||
|
||
|
||
def main() -> None: | ||
return _main(*sys.argv[1:]) | ||
|
||
|
||
if __name__ == "__main__": | ||
# fixme: soft-code | ||
# todo: store prefered 2fas file somewhere | ||
# todo: deal with unencrypted ones | ||
main() |
Oops, something went wrong.