mirror of
https://github.com/Garmelon/PFERD.git
synced 2023-12-21 10:23:01 +01:00
Add pass authenticator
This commit is contained in:
parent
46fb782798
commit
ed24366aba
@ -25,6 +25,7 @@ ambiguous situations.
|
|||||||
### Added
|
### Added
|
||||||
- Download of page descriptions
|
- Download of page descriptions
|
||||||
- Forum download support
|
- Forum download support
|
||||||
|
- `pass` authenticator
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Add `cpp` extension to default `link_regex` of IPD crawler
|
- Add `cpp` extension to default `link_regex` of IPD crawler
|
||||||
|
21
CONFIG.md
21
CONFIG.md
@ -223,6 +223,23 @@ is stored in the keyring.
|
|||||||
- `keyring_name`: The service name PFERD uses for storing credentials. (Default:
|
- `keyring_name`: The service name PFERD uses for storing credentials. (Default:
|
||||||
`PFERD`)
|
`PFERD`)
|
||||||
|
|
||||||
|
### The `pass` authenticator
|
||||||
|
|
||||||
|
This authenticator queries the [`pass` password manager][3] for a username and
|
||||||
|
password. It tries to be mostly compatible with [browserpass][4] and
|
||||||
|
[passff][5], so see those links for an overview of the format. If PFERD fails
|
||||||
|
to load your password, you can use the `--explain` flag to see why.
|
||||||
|
|
||||||
|
- `passname`: The name of the password to use (Required)
|
||||||
|
- `username_prefixes`: A comma-separated list of username line prefixes
|
||||||
|
(Default: `login,username,user`)
|
||||||
|
- `password_prefixes`: A comma-separated list of password line prefixes
|
||||||
|
(Default: `password,pass,secret`)
|
||||||
|
|
||||||
|
[3]: <https://www.passwordstore.org/> "Pass: The Standard Unix Password Manager"
|
||||||
|
[4]: <https://github.com/browserpass/browserpass-extension#organizing-password-store> "Organizing password store"
|
||||||
|
[5]: <https://github.com/passff/passff#multi-line-format> "Multi-line format"
|
||||||
|
|
||||||
### The `tfa` authenticator
|
### The `tfa` authenticator
|
||||||
|
|
||||||
This authenticator prompts the user on the console for a two-factor
|
This authenticator prompts the user on the console for a two-factor
|
||||||
@ -316,7 +333,7 @@ is a regular expression and `TARGET` an f-string based template. If a path
|
|||||||
matches `SOURCE`, the output path is created using `TARGET` as template.
|
matches `SOURCE`, the output path is created using `TARGET` as template.
|
||||||
`SOURCE` is automatically anchored.
|
`SOURCE` is automatically anchored.
|
||||||
|
|
||||||
`TARGET` uses Python's [format string syntax][3]. The *n*-th capturing group can
|
`TARGET` uses Python's [format string syntax][6]. The *n*-th capturing group can
|
||||||
be referred to as `{g<n>}` (e.g. `{g3}`). `{g0}` refers to the original path.
|
be referred to as `{g<n>}` (e.g. `{g3}`). `{g0}` refers to the original path.
|
||||||
If capturing group *n*'s contents are a valid integer, the integer value is
|
If capturing group *n*'s contents are a valid integer, the integer value is
|
||||||
available as `{i<n>}` (e.g. `{i3}`). If capturing group *n*'s contents are a
|
available as `{i<n>}` (e.g. `{i3}`). If capturing group *n*'s contents are a
|
||||||
@ -337,7 +354,7 @@ Example: `f(oo+)/be?ar -re-> B{g1.upper()}H/fear`
|
|||||||
- Converts `fooooo/bear` into `BOOOOOH/fear`
|
- Converts `fooooo/bear` into `BOOOOOH/fear`
|
||||||
- Converts `foo/bar/baz` into `BOOH/fear/baz`
|
- Converts `foo/bar/baz` into `BOOH/fear/baz`
|
||||||
|
|
||||||
[3]: <https://docs.python.org/3/library/string.html#format-string-syntax> "Format String Syntax"
|
[6]: <https://docs.python.org/3/library/string.html#format-string-syntax> "Format String Syntax"
|
||||||
|
|
||||||
### The `-name-re->` arrow
|
### The `-name-re->` arrow
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ from ..config import Config
|
|||||||
from .authenticator import Authenticator, AuthError, AuthLoadError, AuthSection # noqa: F401
|
from .authenticator import Authenticator, AuthError, AuthLoadError, AuthSection # noqa: F401
|
||||||
from .credential_file import CredentialFileAuthenticator, CredentialFileAuthSection
|
from .credential_file import CredentialFileAuthenticator, CredentialFileAuthSection
|
||||||
from .keyring import KeyringAuthenticator, KeyringAuthSection
|
from .keyring import KeyringAuthenticator, KeyringAuthSection
|
||||||
|
from .pass_ import PassAuthenticator, PassAuthSection
|
||||||
from .simple import SimpleAuthenticator, SimpleAuthSection
|
from .simple import SimpleAuthenticator, SimpleAuthSection
|
||||||
from .tfa import TfaAuthenticator
|
from .tfa import TfaAuthenticator
|
||||||
|
|
||||||
@ -19,6 +20,8 @@ AUTHENTICATORS: Dict[str, AuthConstructor] = {
|
|||||||
CredentialFileAuthenticator(n, CredentialFileAuthSection(s), c),
|
CredentialFileAuthenticator(n, CredentialFileAuthSection(s), c),
|
||||||
"keyring": lambda n, s, c:
|
"keyring": lambda n, s, c:
|
||||||
KeyringAuthenticator(n, KeyringAuthSection(s)),
|
KeyringAuthenticator(n, KeyringAuthSection(s)),
|
||||||
|
"pass": lambda n, s, c:
|
||||||
|
PassAuthenticator(n, PassAuthSection(s)),
|
||||||
"simple": lambda n, s, c:
|
"simple": lambda n, s, c:
|
||||||
SimpleAuthenticator(n, SimpleAuthSection(s)),
|
SimpleAuthenticator(n, SimpleAuthSection(s)),
|
||||||
"tfa": lambda n, s, c:
|
"tfa": lambda n, s, c:
|
||||||
|
98
PFERD/auth/pass_.py
Normal file
98
PFERD/auth/pass_.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
from ..logging import log
|
||||||
|
from .authenticator import Authenticator, AuthError, AuthSection
|
||||||
|
|
||||||
|
|
||||||
|
class PassAuthSection(AuthSection):
|
||||||
|
def passname(self) -> str:
|
||||||
|
if (value := self.s.get("passname")) is None:
|
||||||
|
self.missing_value("passname")
|
||||||
|
return value
|
||||||
|
|
||||||
|
def username_prefixes(self) -> List[str]:
|
||||||
|
value = self.s.get("username_prefixes", "login,username,user")
|
||||||
|
return [prefix.lower() for prefix in value.split(",")]
|
||||||
|
|
||||||
|
def password_prefixes(self) -> List[str]:
|
||||||
|
value = self.s.get("password_prefixes", "password,pass,secret")
|
||||||
|
return [prefix.lower() for prefix in value.split(",")]
|
||||||
|
|
||||||
|
|
||||||
|
class PassAuthenticator(Authenticator):
|
||||||
|
PREFIXED_LINE_RE = r"([a-zA-Z]+):\s?(.*)" # to be used with fullmatch
|
||||||
|
|
||||||
|
def __init__(self, name: str, section: PassAuthSection) -> None:
|
||||||
|
super().__init__(name)
|
||||||
|
|
||||||
|
self._passname = section.passname()
|
||||||
|
self._username_prefixes = section.username_prefixes()
|
||||||
|
self._password_prefixes = section.password_prefixes()
|
||||||
|
|
||||||
|
async def credentials(self) -> Tuple[str, str]:
|
||||||
|
log.explain_topic("Obtaining credentials from pass")
|
||||||
|
|
||||||
|
try:
|
||||||
|
log.explain(f"Calling 'pass show {self._passname}'")
|
||||||
|
result = subprocess.check_output(["pass", "show", self._passname], text=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
raise AuthError(f"Failed to get password info from {self._passname}: {e}")
|
||||||
|
|
||||||
|
prefixed = {}
|
||||||
|
unprefixed = []
|
||||||
|
for line in result.strip().splitlines():
|
||||||
|
if match := re.fullmatch(self.PREFIXED_LINE_RE, line):
|
||||||
|
prefix = match.group(1).lower()
|
||||||
|
value = match.group(2)
|
||||||
|
log.explain(f"Found prefixed line {line!r} with prefix {prefix!r}, value {value!r}")
|
||||||
|
if prefix in prefixed:
|
||||||
|
raise AuthError(f"Prefix {prefix} specified multiple times")
|
||||||
|
prefixed[prefix] = value
|
||||||
|
else:
|
||||||
|
log.explain(f"Found unprefixed line {line!r}")
|
||||||
|
unprefixed.append(line)
|
||||||
|
|
||||||
|
username = None
|
||||||
|
for prefix in self._username_prefixes:
|
||||||
|
log.explain(f"Looking for username at prefix {prefix!r}")
|
||||||
|
if prefix in prefixed:
|
||||||
|
username = prefixed[prefix]
|
||||||
|
log.explain(f"Found username {username!r}")
|
||||||
|
break
|
||||||
|
|
||||||
|
password = None
|
||||||
|
for prefix in self._password_prefixes:
|
||||||
|
log.explain(f"Looking for password at prefix {prefix!r}")
|
||||||
|
if prefix in prefixed:
|
||||||
|
password = prefixed[prefix]
|
||||||
|
log.explain(f"Found password {password!r}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if password is None and username is None:
|
||||||
|
log.explain("No username and password found so far")
|
||||||
|
log.explain("Using first unprefixed line as password")
|
||||||
|
log.explain("Using second unprefixed line as username")
|
||||||
|
elif password is None:
|
||||||
|
log.explain("No password found so far")
|
||||||
|
log.explain("Using first unprefixed line as password")
|
||||||
|
elif username is None:
|
||||||
|
log.explain("No username found so far")
|
||||||
|
log.explain("Using first unprefixed line as username")
|
||||||
|
|
||||||
|
if password is None:
|
||||||
|
if not unprefixed:
|
||||||
|
log.explain("Not enough unprefixed lines left")
|
||||||
|
raise AuthError("Password could not be determined")
|
||||||
|
password = unprefixed.pop(0)
|
||||||
|
log.explain(f"Found password {password!r}")
|
||||||
|
|
||||||
|
if username is None:
|
||||||
|
if not unprefixed:
|
||||||
|
log.explain("Not enough unprefixed lines left")
|
||||||
|
raise AuthError("Username could not be determined")
|
||||||
|
username = unprefixed.pop(0)
|
||||||
|
log.explain(f"Found username {username!r}")
|
||||||
|
|
||||||
|
return username, password
|
Loading…
Reference in New Issue
Block a user