Avendesora Collaborative Password Manager
=========================================
*Avendesora, the leaf of the Tree of Life is the key.*
.. image:: https://pepy.tech/badge/avendesora/month
:target: https://pepy.tech/project/avendesora
.. image:: https://img.shields.io/readthedocs/avendesora.svg
:target: https://avendesora.readthedocs.io/en/latest/?badge=latest
.. image:: https://github.com/KenKundert/avendesora/actions/workflows/build.yaml/badge.svg
:target: https://github.com/KenKundert/avendesora/actions/workflows/build.yaml
.. image:: https://img.shields.io/coveralls/KenKundert/avendesora.svg
:target: https://coveralls.io/r/KenKundert/avendesora
.. image:: https://img.shields.io/pypi/v/avendesora.svg
:target: https://pypi.python.org/pypi/avendesora
.. image:: https://img.shields.io/pypi/pyversions/avendesora.svg
:target: https://pypi.python.org/pypi/avendesora/
:Authors: Ken & Kale Kundert
:Version: 1.26
:Released: 2024-08-28
Avendesora replaces Abraxas, which are both alternatives to the traditional
password vault.
Please report all bugs and suggestions to avendesora@nurdletech.com
Introduction
------------
Avendesora is powerful command-line utility that can securely hold and
conveniently provide access to a wide variety of information about your
accounts, including its secrets such as passwords. Account values can be
displayed, copied to the clipboard, or automatically typed into running
applications such as your web browser or terminal windows. Avendesora can also
open accounts in your web browser, automatically recognize which account to use
based on the window title, and warn you if the browser is not using encryption
when you go to enter your password.
Account secrets can be saved in encrypted form, as with password vaults, or
generated from a root secret. Generated secrets have two important benefits.
First, they are produced from a random seed, and so are quite unpredictable.
This is important, because the predictability of a passwords can be exploited
when cracking passwords. Second, if a root secret is shared with another
trusted party, then you both can generate new shared secrets without passing any
further secrets.
Secrets are generated from a collection of seeds, one of which must be random
with a very high degree of entropy. The random seed is referred to as the
'master seed' or the 'root seed'. It is extremely important that the master
seed remain completely secure. Never disclose a master seed to anyone except
for a person you wish to collaborate with, and then only used the shared master
seed for shared secrets. All of your private secrets should be generated from
private master seeds. The seeds generally include the master seed, the account
name, the secret name, and perhaps a version name. For example, imagine having
a Gmail account, then the account name might simply be 'gmail', and the secret
name might be 'passcode'. In this case, your master seed is combined with the
words 'gmail' and 'passcode', the combination is hashed, and then password is
generated with an appropriate recipe that you specify. There are recipes for
passwords, pass phrases, PINs, security questions, etc. The password itself is
not stored, rather it is the seeds that are stored and the password is
regenerated when needed. Notice that all the seeds except the master seed need
not be kept secure. Thus, once you have shared a master seed with
a collaborator, all you need to do is share the remaining seeds and your
collaborator can generate exactly the same password. Another important thing to
notice is that the generated password is dependent on the account and secret
names. Thus if you rename your account or your secret, the password will change.
So you should be careful when you first create your account to name it
appropriately so you don't feel the need to change it in the future. For
example, 'gmail' might not be a good account name if you expect to have multiple
Gmail accounts. In this case you might want to include your username in the
account name. You can always make the shorter 'gmail' as an account alias so you
can still access the account quickly.
Installation
------------
Install with::
pip3 install --user avendesora
This will place avendesora in ~/.local/bin, which should be added to your path.
You will also need to install some operating system commands. On Fedora use::
dnf install gnupg2 xdotool xsel gobject-introspection-devel cairo-gobject-devel
If you would like to use scrypt as a way of encrypting fields, you will need to
install scrypt by hand using::
pip3 install --user scrypt
Upgrading
---------
Avendesora is primarily a password generator. As a result, there is always
a chance that something could change in the password generation algorithm that
causes the generated passwords to change. Of course, the program is thoroughly
tested to assure this does not happen, but there is still a small chance that
something slips through. To assure that you are not affected by this, you
should archive your passwords before you upgrade with::
avendesora changed
avendesora archive
The *changed* command should always be run before an *archive* command. It
allows you to review all the changes that have occurred so that you can verify
that they were all intentional. Once you are comfortable, run the *archive*
command to save all the changes. Then upgrade with::
pip3 install -upgrade --user avendesora
Finally, run::
avendesora changed
to confirm that none of your generated passwords have changed.
It is a good idea to run 'avendesora changed' and 'avendesora archive' on
a routine basis to keep your archive up to date.
Upon updating you may find that Avendesora produces a message that a 'hash' has
changed. This is an indication that something has changed in the program that
could affect the generated secrets. Again, care is taken when developing
Avendesora to prevent this from happening. But it is an indication that you
should take extra care. Specifically you should follow the above procedure to
assure that the value of your generated secrets have not changed. Once you have
confirmed that the upgrade has not affected your generated secrets, you should
follow the directions given in the warning and update the appropriate hash
contained in ~/.config/avendesora/.hashes.
Requirements
------------
GPG
"""
To use Avendesora, you will need GPG and you will need a GPG ID that is
associated with a private key. That GPG ID could be in the form of an email
address or an ID string that can be found using 'gpg --list-keys'.
If you do not yet have a GPG key, you can get one using::
$ gpg --gen-key
You should probably choose 4096 RSA keys. Now, edit ~/.gnupg/gpg-conf and add
the line::
use-agent
That way, you generally need to give your GPG key pass phrase less often. The
agent remembers the passphrase for you for a time. Ten minutes is the default,
but you can configure gpg-agent to cache passphrases for as long as you like.
If you use the agent, be sure to also use screen locking so your passwords are
secure when you walk away from your computer.
Vim
"""
If you use Vim, it is very helpful for you to install GPG support in Vim. To do
so first download::
http://www.vim.org/scripts/script.php?script_id=3645
Then copy the file into your Vim configuration hierarchy::
cp gnupg.vim ~/.vim/plugin
Initialization
--------------
To operate, Avendesora needs a collection of configuration and accounts files
that are stored in ~/.config/avendesora. To create this directory and the
initial versions of these files, run::
avendesora init -g <gpg_id>
For example::
avendesora init -g bob@nurdletech.com
or::
avendesora init -g 1B2AFA1C
If you would like to have more than one person access your passwords, you should
give GPG IDs for everyone::
avendesora init -g bob@nurdletech.com,rob@nurdletech.com
After initialization, there should be several files in ~/.config/avendesora. In
particular, you should see at least an initial accounts files and a config file.
Configuration
-------------
The config file (~/.config/avendesora/config) allows you to personalize
Avendesora to your needs. After initializing your account you should take the
time to review the config file and adjust it to fit your needs. You should be
very thoughtful in this initial configuration, because some decisions (or
nondecisions) you make can be very difficult to change later. The reason for
this is that they may affect the passwords you generate, and if you change them
you may change existing generated passwords. In particular, be careful with
*dictionary_file* and *default_passphase_separator*. Changing these values when
first initializing Avendesora is fine, but should not be done or done very
carefully once you start creating accounts and secrets.
During an initial configuration is also a convenient time to determine which of
your files should be encrypted with GPG. To assure that a file is encrypted,
give it a GPG file suffix (.gpg or .asc). The appropriate settings to adjust
are: *archive_file*, *log_file*, both of which are set in the config file, and
the accounts files, which are found in ~/.config/avendesora/.accounts_files. For
security reasons it is highly recommended that the archive file be encrypted,
and any accounts file that contain sensitive accounts. If you change the suffix
on an accounts file and you have not yet placed any accounts in that file, you
can simply delete the existing file and then regenerate it using::
avendesora init -g <gpg_id>
Any files that already exist will not be touched, but any missing files will be
recreated, and this time they will be encrypted or not based on the extension
you give.
Using Avendesora
----------------
Avendesora supports a series of commands, the complete list of which can be had
by running the help command::
> avendesora help
More information on a command is accessed by adding the name of the command as
the second argument to the help command::
> avendesora help name
As an aid to finding the right help topic the topics that contain a particular
search term are listed by adding the -s or --search command line option::
> avendesora help -s term
If the first argument is not a command, then it must be the name of an account.
In this case, the *credentials* command is run if only the account name is
given, otherwise the *value* command is run (any options to the value command
should be given after the account name). The *credentials* command generally
gives the information you would need to login to an account, typically the
username or email and the passcode. The *value* command allows you to request
the value of a specific piece of information from the account. So for example::
> avendesora amazon
email: albert@ricochet.com
password: XDyfL5it
> avendesora citi pin
56713522
> avendesora southwest 0
questions.0 (First foreign country I visited): contour subtract impel
If you give a number for the desired value, Avendesora assumes you want the
answer to the corresponding security question.
Accounts
--------
Avendesora holds information about your accounts in accounts files. The list of
current accounts files is contained in ~/.config/avendesora/.accounts_files.
Each is a possibly encrypted Python file. All information known about
a particular account is contained in the attributes of a class that is created
for that account. For example:
.. code-block:: python
class BigBank(Account):
aliases = 'bb'
username = 'gman33'
email = 'gman33@pizza.com'
urls = 'https://bigbank.com/login'
passcode = Password(length=12)
verbal = Passphrase(length=2)
pin = PIN()
accounts = {
'checking': Hidden('MTIzNDU2Nzg='),
'savings': Hidden('MjM0NTY3ODk='),
'creditcard': Hidden('ODczMi0yODk0LTI4NjEtMjgxMA=='),
}
questions = [
Question('What city were you born in?'),
Question('What street did you grow up on?'),
Question('What was your childhood nickname?'),
]
customer_service = '1-866-229-6633'
Each attribute represents a piece of information that can be requested. For
example, a summary of all information can be requested with::
> avendesora values bb
names: bigbank, bb
accounts:
checking: <reveal with 'avendesora show bigbank accounts.checking'>
creditcard: <reveal with 'avendesora show bigbank accounts.creditcard'>
savings: <reveal with 'avendesora show bigbank accounts.savings'>
customer service: 1-866-229-6633
email: gman33@pizza.com
passcode: <reveal with 'avendesora show bigbank passcode'>
pin: <reveal with 'avendesora show bigbank pin'>
questions:
0: What city were you born in? <reveal with 'avendesora show bigbank questions.0'>
1: What street did you grow up on? <reveal with 'avendesora show bigbank questions.1'>
2: What was your childhood nickname? <reveal with 'avendesora show bigbank questions.2'>
urls: https://bigbank.com/login
username: gman33
verbal: <reveal with 'avendesora show bigbank verbal'>
The attributes have various levels of confidentiality. Simple strings are not
considered sensitive. Those values provided by Python classes inherit the
confidentiality of the class. Hide() and Hidden() provides simple concealment.
GPG() and Scrypt() provides full encryption. And classes like Password(),
PasswordRecipe(), Passphrase(), PIN() and Question() generate secrets.
Attributes that are considered sensitive are not shown in the above summary, but
can be requested individually::
> avendesora value bb pin
pin: 7784
Attributes can be simple scalars, such as *pin*. They can be arrays, such as
*questions*::
> avendesora value bigbank questions.1
questions.1 (What street did you grow up on?): lockout insulator crumb
Or they can be dictionaries::
> avendesora value bb accounts.checking
accounts.checking: 12345678
The passcode attribute is the default scalar attribute::
> avendesora value bb
passcode: Nj3gpqHNfiie
The questions attribute is the default array attribute, which is used if the
requested field is a number::
> avendesora value bb 0
questions.0 (What city were you born in?): muffin favorite boyfriend
You can also use simple scripts as the requested value::
> avendesora value 'username: {username}, password: {passcode}'
username: gman33, password: Nj3gpqHNfiie
Finally, the attributes themselves may be scripts. For example, if you added the
following to you account::
cc = Script('{accounts.creditcard} 02/23 363')
Then you could access a summary of your credit card information with::
> avendesora value cc
8732-2894-2861-2810 02/23 363
Adding And Editing Accounts
---------------------------
You add new accounts using the *add* command::
> avendesora add [<template>]
The available templates can be found using::
> avendesora help add
You can add new templates or edit the existing templates by changing
*account_templates* in ~/.config/avendesora/config.
The *add* command will open your editor (set this with the *edit_template*
setting in the config file). If you are using default version of *edit_template*
the template will be opened in Vim with the *n* key is mapped to take you to the
next field. You can edit any part of the template you like, but at a minimum you
need to edit the fields.
Once an account exists, you can edit it using::
> avendesora edit [<account>]
This opens the accounts file with your editor (set this with the *edit_account*
setting in the config file). If you are using default version of *edit_account*,
which uses VIM, it should take you directly to the account.
Finding Accounts
----------------
There are two ways of finding accounts. First, you can list any accounts whose
name or aliases contains a text fragment. For example::
> avendesora find bank
bank:
bankofamerica (boa)
Second, you can list any accounts that contain a text fragment in any non-secret
field. For example::
> avendesora search 4408
4408:
bankofamerica (boa)
Autotyping Passwords
--------------------
There are a couple of things that must be done to enable autotyping of
passwords. First, at least some secrets must be configured for discovery.
Discovery allows secrets to determine whether they are good candidates for use
in a particular situation based on the environment. The environment includes
such things as with title of the active window, the user name, the host name,
etc. If multiple secrets are suitable, a small window pops up and lets you
choose between them. To see how to configure secrets for discovery, run
'avendesora help discovery'.
To make secret discovery easier and more robust it is helpful to add a plugin to
your web browser to make its title more informative. For Firefox, the best
plugin to use is *AddURLToWindowTitle*. For Chrome it is *URLinTitle*. (The
latest versions of Firefox are incompatible with *AddURLToWindowTitle*, however
you can use the Firefox version of *URLinTitle* instead.) It is recommended that
you install the appropriate one into your browser. For AddURLToWindowTitle, set
the following options:
| show full URL = yes
| separator string = '-'
| show field attributes = no
For URLinTitle, set:
| tab title format = '{title} - {protocol}://{hostname}{port}/{path}'
Finally, you need to configure your window manager to run Avendesora when you
type a special hot key, such as ``Alt p``. The idea is that you are in
a situation where you need a secret, such as visiting your bank's website in
your browser, then you click on the account name field with your mouse and type
your hot key. This runs Avendesora without an account name. In this case,
Avendesora uses secret discovery to determine which secret to use and the script
that should be used to produce the required information. Generally the script
would be to enter the account name, then tab, then the password, and finally
return, but you can configure the script as you choose. This is all done as part
of configuring discovery. The method for associating Avendesora to a particular
hot key is dependent on your window manager. With Gnome, it requires that you
open your Keyboard Shortcuts preferences and create a new shortcut. When you do
this, choose 'avendesora value' as the command to run.
Python API
----------
You can access account information from Avendesora using Python using a simple
relatively high-level interface as shown in this example:
.. code-block:: python
from avendesora import PasswordGenerator, PasswordError
from inform import display, fatal, os_error
from shlib import Run
from pathlib import Path
try:
pw = PasswordGenerator()
account = pw.get_account('mybank')
name = account.get_value('NAME')
username = account.get_value('username')
passcode = account.get_value('passcode')
url = account.get_value('ofxurl')
except PasswordError as err:
fatal(err)
try:
curl = Run(f'curl --user {username!s}:{passcode!s} {url!s}', 'sOEW0')
Path(f'{name!s}.ofx').write_text(curl.stdout)
except OSError as err:
fatal(os_error(err))
PasswordGenerator():
Initializes the password generator. You should pass no arguments.
get_account(name, request_seed=False, stealth_name=None):
Accesses a particular account. Takes a string for the account name or alias.
The name is case insensitive and the '-' may be given for '_'.
Optionally takes a second argument (*request_seed*) that may be a Boolean,
a string, or a function that returns a string. The string is used as an
additional seed (see: `avendesora help misdirection`), and if True is passed
in, the user in queried for the seed.
The stealth name is used as account name if the account is a stealth
account.
get_name():
return name of account.
get_value(field):
Returns the value of a particular account attribute given a user-oriented
string that describes the desired attribute. The value requested must be
a scalar value, meaning that you must individually request members of arrays
or dictionary attributes. Here are some examples that demonstrate the various
ways of accessing the various kinds of attributes:
.. code-block:: python
passcode = account.get_value()
username = account.get_value('username')
both = account.get_value('username: {username}, password: {passcode}')
checking = account.get_value('accounts.checking')
savings = account.get_value('accounts[checking]')
answer0 = account.get_value(0)
answer1 = account.get_value('questions.1')
answer2 = account.get_value('questions[2]')
If the argument passed to get_value is a field, then it may consist of
a name (the identifier for the first level of the field) and a key (the
identifier for the second level of the field). The field is case insensitive
and a '-' will match a '_' and visa versa.
You can also specify the name and key separately in a tuple:
.. code-block:: python
username = account.get_value(('username',))
checking = account.get_value(('accounts', 'checking'))
answer0 = account.get_value((0,))
answer1 = account.get_value(('questions', 1))
The value is returned as an object that contains four attributes, value (the
actual value), is_secret (whether the value is secret or contains a secret),
name (the name of the value), and desc (the description, contains the actual
question of the answer to a question is requested). Converting the object
to a string returns the value rendered as a string. There is also the
render() method that returns a string that combines the name and the
description with the value. It takes an optional collection of format
strings, the first one that matches is used. The format strings may contain
keys in braces that get replaced by the corresponding attributes. The known
keys are n {name}, k (key), f (field, combination of name and key),
d (description) and v (value). A format string does not match it if
contains a key for a value that is not available. If no format string
matches, the value is returned as a string. The default formats are ('{f}
({d}): {v}', '{f}: {v}').
If a composite field is requested get_value() raises a PasswordError, and
the exception contains the *is_collection* and *collection* attributes. The
first is a Boolean and the second is the list of available keys.
PassworError returns None for unknown attributes, so it is always safe to
access these attributes without checking whether they exist.
get_values(field):
Used to get the values for a composite field. It iterates through the value
and returns a tuple that contains the key and the value for each item in the
field.
Field is an identifier that may consist of a name (the identifier for the
first level of the field) and a key (the identifier for the second level of
the field). The field is case insensitive and a '-' will match a '_' and
visa versa.
Here is how you might iterate through both the scalar and composite values
in an account:
.. code-block:: python
try:
value = acct.get_value(field)
lines += value.render('{n}: {v}').split('\n')
except PasswordError as e:
if not e.is_collection:
raise
lines += [name + ':']
for key, value in acct.get_values(name):
lines += indent(
value.render(('{k}) {d}: {v}', '{k}: {v}'))
).split('\n')
get_fields():
Iterates through the fields, each iteration yields a name and possibly
a collection of keys ([None] is returned if the name corresponds to
a scalar). The name and keys returned are the resolved names, which can be
passed to get_scalar() and get_composite().
Here is how this method can be used to iterate through the account values:
.. code-block:: python
# gather user fields
lines = []
for field, keys in account.get_fields():
if keys == [None]:
v = account.get_value(field)
lines += v.render('{n}: {v}').split('\n')
else:
lines.append(field + ':')
for k, v in account.get_values(field):
lines += indent(
v.render(('{k}) {d}: {v}', '{k}: {v}'))
).split('\n')
account_summary = '\n'.join(lines)
get_fields() accepts a Boolean argument that if specified and is true will
iterate through all fields, including those generally only used by
Avendesora, such as aliases and discovery.
get_scalar(name, key=None, default=False):
A lower level interface than get_value that given a name and perhaps a key
returns a scalar value. Also takes an optional default value that is
returned if the value is not found. Unlike get_value, the actual value is
returned, not a object that contains multiple facets of the value. Also, the
name and key must match exactly.
The name is the field name, and the key would identity which value is
desired if the field is a composite. If default is False, an error is raise
if the value is not present, otherwise the default value itself is returned.
If the value returned is an Avendesora object (GeneratedSecret,
ObscuredSecret, Script), then you should cast it to a string to get its
resolved value.
get_composite(name):
A lower level interface than get_value that given a name returns the value
of the associated field, which may be a scalar (string or integer) or
a composite (array of dictionary). Unlike get_value, the actual value is
returned, not a object that contains multiple facets of the value. Also,
the name and key must match exactly.
If the value returned is an Avendesora object (GeneratedSecret,
ObscuredSecret, Script), then you should cast it to a string to get its
resolved value.
API Example
-----------
The following example creates encrypted files that contain account information
that would be needed by close family members and by a business partner in case
anything happened to you. This is an abbreviated version of an example given in
the users' guide.
.. code-block:: python
#!/bin/env python3
from avendesora import PasswordGenerator, PasswordError
from textwrap import dedent
from inform import (
display, done, Error, error, indent, is_collection, os_error
)
import gnupg
files = [
{ 'FILENAME': 'family.gpg',
'RECIPIENTS': 'me@home.com son@home.com daughter@home.com'.split(),
'ACCOUNTS': 'bank brokerage creditcard'.split(),
},
{ 'FILENAME': 'partner.gpg',
'RECIPIENTS': 'me@work.com partner@work.com'.split(),
'ACCOUNTS': 'login ssh root backups'.split(),
},
]
try:
pw = PasswordGenerator()
for each in files:
accounts = []
for account_name in each['ACCOUNTS']:
acct = pw.get_account(account_name)
title = acct.get_scalar('desc', default=account_name)
lines = [title, len(title)*'=']
for name, keys in acct.get_fields():
if keys:
lines.append(name + ':')
for key, value in acct.get_values(name):
lines += indent(
value.render(('{k}) {d}: {v}', '{k}: {v}'))
).split('\n')
else:
value = acct.get_value(name)
lines += value.render('{n}: {v}').split('\n')
accounts.append('\n'.join(lines))
gpg = gnupg.GPG(gpgbinary='gpg2')
encrypted = gpg.encrypt('\n\n\n'.join(accounts), each['RECIPIENTS'])
if not encrypted.ok:
raise Error(
'unable to encrypt:', encrypted.stderr, culprit=each['FILENAME']
)
try:
with open(each['FILENAME'], 'w') as file:
file.write(str(encrypted))
print("%s: created." % each['FILENAME'])
except OSError as e:
raise Error(os_error(e))
except (PasswordError, Error) as e:
e.terminate()
Getting Help
------------
You can find the documentation on `ReadTheDocs <https://avendesora.readthedocs.io>`_.
The *help* command provides information on how to use Avendesora's various
features. To get a listing of the topics available, use::
avendesora help
Then, for information on a specific topic use::
avendesora help <topic>
It is worth browsing all of the available topics at least once to get a sense of
all that Avendesora can do.
Contributing
------------
Please ask questions or report bugs on `Github Issues
<https://github.com/KenKundert/avendesora/issues>`_. I will entertain pull
requests if you make improvements. Currently *Avendesora* is very *Fedora* and
*VIM* centric. I am particularly interested in help adapting *Avendesora* in the
following ways:
- Support for other editors, window managers and distributions.
- Support for Windows and OSX.
- Support for Android and iOS (perhaps through exports to a password manager
that already support smartphones).
Raw data
{
"_id": null,
"home_page": null,
"name": "avendesora",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.6",
"maintainer_email": null,
"keywords": "avendesora, password, password vault, XKCD",
"author": "Ken Kundert",
"author_email": "avendesora@nurdletech.com",
"download_url": "https://files.pythonhosted.org/packages/cc/8e/73f41fc976ca77e93dbbbbd34552f531daa3a610193eae36da1f3e469ced/avendesora-1.26.tar.gz",
"platform": null,
"description": "Avendesora Collaborative Password Manager\n=========================================\n\n*Avendesora, the leaf of the Tree of Life is the key.*\n\n.. image:: https://pepy.tech/badge/avendesora/month\n :target: https://pepy.tech/project/avendesora\n\n.. image:: https://img.shields.io/readthedocs/avendesora.svg\n :target: https://avendesora.readthedocs.io/en/latest/?badge=latest\n\n.. image:: https://github.com/KenKundert/avendesora/actions/workflows/build.yaml/badge.svg\n :target: https://github.com/KenKundert/avendesora/actions/workflows/build.yaml\n\n.. image:: https://img.shields.io/coveralls/KenKundert/avendesora.svg\n :target: https://coveralls.io/r/KenKundert/avendesora\n\n.. image:: https://img.shields.io/pypi/v/avendesora.svg\n :target: https://pypi.python.org/pypi/avendesora\n\n.. image:: https://img.shields.io/pypi/pyversions/avendesora.svg\n :target: https://pypi.python.org/pypi/avendesora/\n\n\n:Authors: Ken & Kale Kundert\n:Version: 1.26\n:Released: 2024-08-28\n\nAvendesora replaces Abraxas, which are both alternatives to the traditional \npassword vault.\n\nPlease report all bugs and suggestions to avendesora@nurdletech.com\n\nIntroduction\n------------\n\nAvendesora is powerful command-line utility that can securely hold and \nconveniently provide access to a wide variety of information about your \naccounts, including its secrets such as passwords. Account values can be \ndisplayed, copied to the clipboard, or automatically typed into running \napplications such as your web browser or terminal windows. Avendesora can also \nopen accounts in your web browser, automatically recognize which account to use \nbased on the window title, and warn you if the browser is not using encryption \nwhen you go to enter your password.\n\nAccount secrets can be saved in encrypted form, as with password vaults, or \ngenerated from a root secret. Generated secrets have two important benefits. \nFirst, they are produced from a random seed, and so are quite unpredictable. \nThis is important, because the predictability of a passwords can be exploited \nwhen cracking passwords. Second, if a root secret is shared with another \ntrusted party, then you both can generate new shared secrets without passing any \nfurther secrets.\n\nSecrets are generated from a collection of seeds, one of which must be random \nwith a very high degree of entropy. The random seed is referred to as the \n'master seed' or the 'root seed'. It is extremely important that the master \nseed remain completely secure. Never disclose a master seed to anyone except \nfor a person you wish to collaborate with, and then only used the shared master \nseed for shared secrets. All of your private secrets should be generated from \nprivate master seeds. The seeds generally include the master seed, the account \nname, the secret name, and perhaps a version name. For example, imagine having \na Gmail account, then the account name might simply be 'gmail', and the secret \nname might be 'passcode'. In this case, your master seed is combined with the \nwords 'gmail' and 'passcode', the combination is hashed, and then password is \ngenerated with an appropriate recipe that you specify. There are recipes for \npasswords, pass phrases, PINs, security questions, etc. The password itself is \nnot stored, rather it is the seeds that are stored and the password is \nregenerated when needed. Notice that all the seeds except the master seed need \nnot be kept secure. Thus, once you have shared a master seed with \na collaborator, all you need to do is share the remaining seeds and your \ncollaborator can generate exactly the same password. Another important thing to \nnotice is that the generated password is dependent on the account and secret \nnames. Thus if you rename your account or your secret, the password will change. \nSo you should be careful when you first create your account to name it \nappropriately so you don't feel the need to change it in the future. For \nexample, 'gmail' might not be a good account name if you expect to have multiple \nGmail accounts. In this case you might want to include your username in the \naccount name. You can always make the shorter 'gmail' as an account alias so you \ncan still access the account quickly.\n\n\nInstallation\n------------\n\nInstall with::\n\n pip3 install --user avendesora\n\nThis will place avendesora in ~/.local/bin, which should be added to your path.\n\nYou will also need to install some operating system commands. On Fedora use::\n\n dnf install gnupg2 xdotool xsel gobject-introspection-devel cairo-gobject-devel\n\nIf you would like to use scrypt as a way of encrypting fields, you will need to \ninstall scrypt by hand using::\n\n pip3 install --user scrypt\n\n\nUpgrading\n---------\n\nAvendesora is primarily a password generator. As a result, there is always \na chance that something could change in the password generation algorithm that \ncauses the generated passwords to change. Of course, the program is thoroughly \ntested to assure this does not happen, but there is still a small chance that \nsomething slips through. To assure that you are not affected by this, you \nshould archive your passwords before you upgrade with::\n\n avendesora changed\n avendesora archive\n\nThe *changed* command should always be run before an *archive* command. It \nallows you to review all the changes that have occurred so that you can verify \nthat they were all intentional. Once you are comfortable, run the *archive* \ncommand to save all the changes. Then upgrade with::\n\n pip3 install -upgrade --user avendesora\n\nFinally, run::\n\n avendesora changed\n\nto confirm that none of your generated passwords have changed.\n\nIt is a good idea to run 'avendesora changed' and 'avendesora archive' on \na routine basis to keep your archive up to date.\n\nUpon updating you may find that Avendesora produces a message that a 'hash' has \nchanged. This is an indication that something has changed in the program that \ncould affect the generated secrets. Again, care is taken when developing \nAvendesora to prevent this from happening. But it is an indication that you \nshould take extra care. Specifically you should follow the above procedure to \nassure that the value of your generated secrets have not changed. Once you have \nconfirmed that the upgrade has not affected your generated secrets, you should \nfollow the directions given in the warning and update the appropriate hash \ncontained in ~/.config/avendesora/.hashes.\n\n\nRequirements\n------------\n\nGPG\n\"\"\"\nTo use Avendesora, you will need GPG and you will need a GPG ID that is \nassociated with a private key. That GPG ID could be in the form of an email \naddress or an ID string that can be found using 'gpg --list-keys'.\n\nIf you do not yet have a GPG key, you can get one using::\n\n $ gpg --gen-key\n\nYou should probably choose 4096 RSA keys. Now, edit ~/.gnupg/gpg-conf and add \nthe line::\n\n use-agent\n\nThat way, you generally need to give your GPG key pass phrase less often. The \nagent remembers the passphrase for you for a time. Ten minutes is the default, \nbut you can configure gpg-agent to cache passphrases for as long as you like.\n\nIf you use the agent, be sure to also use screen locking so your passwords are \nsecure when you walk away from your computer.\n\n\nVim\n\"\"\"\n\nIf you use Vim, it is very helpful for you to install GPG support in Vim. To do \nso first download::\n\n http://www.vim.org/scripts/script.php?script_id=3645\n\nThen copy the file into your Vim configuration hierarchy::\n\n cp gnupg.vim ~/.vim/plugin\n\n\nInitialization\n--------------\n\nTo operate, Avendesora needs a collection of configuration and accounts files \nthat are stored in ~/.config/avendesora. To create this directory and the \ninitial versions of these files, run::\n\n avendesora init -g <gpg_id>\n\nFor example::\n\n avendesora init -g bob@nurdletech.com\n\nor::\n\n avendesora init -g 1B2AFA1C\n\nIf you would like to have more than one person access your passwords, you should \ngive GPG IDs for everyone::\n\n avendesora init -g bob@nurdletech.com,rob@nurdletech.com\n\nAfter initialization, there should be several files in ~/.config/avendesora. In \nparticular, you should see at least an initial accounts files and a config file.\n\n\nConfiguration\n-------------\n\nThe config file (~/.config/avendesora/config) allows you to personalize \nAvendesora to your needs. After initializing your account you should take the \ntime to review the config file and adjust it to fit your needs. You should be \nvery thoughtful in this initial configuration, because some decisions (or \nnondecisions) you make can be very difficult to change later. The reason for \nthis is that they may affect the passwords you generate, and if you change them \nyou may change existing generated passwords. In particular, be careful with \n*dictionary_file* and *default_passphase_separator*. Changing these values when \nfirst initializing Avendesora is fine, but should not be done or done very \ncarefully once you start creating accounts and secrets.\n\nDuring an initial configuration is also a convenient time to determine which of \nyour files should be encrypted with GPG. To assure that a file is encrypted, \ngive it a GPG file suffix (.gpg or .asc). The appropriate settings to adjust \nare: *archive_file*, *log_file*, both of which are set in the config file, and \nthe accounts files, which are found in ~/.config/avendesora/.accounts_files. For \nsecurity reasons it is highly recommended that the archive file be encrypted, \nand any accounts file that contain sensitive accounts. If you change the suffix \non an accounts file and you have not yet placed any accounts in that file, you \ncan simply delete the existing file and then regenerate it using::\n\n avendesora init -g <gpg_id>\n\nAny files that already exist will not be touched, but any missing files will be \nrecreated, and this time they will be encrypted or not based on the extension \nyou give.\n\n\nUsing Avendesora\n----------------\n\nAvendesora supports a series of commands, the complete list of which can be had \nby running the help command::\n\n > avendesora help\n\nMore information on a command is accessed by adding the name of the command as \nthe second argument to the help command::\n\n > avendesora help name\n\nAs an aid to finding the right help topic the topics that contain a particular \nsearch term are listed by adding the -s or --search command line option::\n\n > avendesora help -s term\n\nIf the first argument is not a command, then it must be the name of an account. \nIn this case, the *credentials* command is run if only the account name is \ngiven, otherwise the *value* command is run (any options to the value command \nshould be given after the account name). The *credentials* command generally \ngives the information you would need to login to an account, typically the \nusername or email and the passcode. The *value* command allows you to request \nthe value of a specific piece of information from the account. So for example::\n\n > avendesora amazon\n email: albert@ricochet.com\n password: XDyfL5it\n\n > avendesora citi pin\n 56713522\n\n > avendesora southwest 0\n questions.0 (First foreign country I visited): contour subtract impel\n\nIf you give a number for the desired value, Avendesora assumes you want the \nanswer to the corresponding security question.\n\n\nAccounts\n--------\n\nAvendesora holds information about your accounts in accounts files. The list of \ncurrent accounts files is contained in ~/.config/avendesora/.accounts_files. \nEach is a possibly encrypted Python file. All information known about \na particular account is contained in the attributes of a class that is created \nfor that account. For example:\n\n.. code-block:: python\n\n class BigBank(Account):\n aliases = 'bb'\n username = 'gman33'\n email = 'gman33@pizza.com'\n urls = 'https://bigbank.com/login'\n passcode = Password(length=12)\n verbal = Passphrase(length=2)\n pin = PIN()\n accounts = {\n 'checking': Hidden('MTIzNDU2Nzg='),\n 'savings': Hidden('MjM0NTY3ODk='),\n 'creditcard': Hidden('ODczMi0yODk0LTI4NjEtMjgxMA=='),\n }\n questions = [\n Question('What city were you born in?'),\n Question('What street did you grow up on?'),\n Question('What was your childhood nickname?'),\n ]\n customer_service = '1-866-229-6633'\n\nEach attribute represents a piece of information that can be requested. For \nexample, a summary of all information can be requested with::\n\n > avendesora values bb\n names: bigbank, bb\n accounts:\n checking: <reveal with 'avendesora show bigbank accounts.checking'>\n creditcard: <reveal with 'avendesora show bigbank accounts.creditcard'>\n savings: <reveal with 'avendesora show bigbank accounts.savings'>\n customer service: 1-866-229-6633\n email: gman33@pizza.com\n passcode: <reveal with 'avendesora show bigbank passcode'>\n pin: <reveal with 'avendesora show bigbank pin'>\n questions:\n 0: What city were you born in? <reveal with 'avendesora show bigbank questions.0'>\n 1: What street did you grow up on? <reveal with 'avendesora show bigbank questions.1'>\n 2: What was your childhood nickname? <reveal with 'avendesora show bigbank questions.2'>\n urls: https://bigbank.com/login\n username: gman33\n verbal: <reveal with 'avendesora show bigbank verbal'>\n\nThe attributes have various levels of confidentiality. Simple strings are not \nconsidered sensitive. Those values provided by Python classes inherit the \nconfidentiality of the class. Hide() and Hidden() provides simple concealment. \nGPG() and Scrypt() provides full encryption. And classes like Password(), \nPasswordRecipe(), Passphrase(), PIN() and Question() generate secrets. \nAttributes that are considered sensitive are not shown in the above summary, but \ncan be requested individually::\n\n > avendesora value bb pin\n pin: 7784\n\nAttributes can be simple scalars, such as *pin*. They can be arrays, such as \n*questions*::\n\n > avendesora value bigbank questions.1\n questions.1 (What street did you grow up on?): lockout insulator crumb\n\nOr they can be dictionaries::\n\n > avendesora value bb accounts.checking\n accounts.checking: 12345678\n\nThe passcode attribute is the default scalar attribute::\n\n > avendesora value bb\n passcode: Nj3gpqHNfiie\n\nThe questions attribute is the default array attribute, which is used if the \nrequested field is a number::\n\n > avendesora value bb 0\n questions.0 (What city were you born in?): muffin favorite boyfriend\n\nYou can also use simple scripts as the requested value::\n\n > avendesora value 'username: {username}, password: {passcode}'\n username: gman33, password: Nj3gpqHNfiie\n\nFinally, the attributes themselves may be scripts. For example, if you added the \nfollowing to you account::\n\n cc = Script('{accounts.creditcard} 02/23 363')\n\nThen you could access a summary of your credit card information with::\n\n > avendesora value cc\n 8732-2894-2861-2810 02/23 363\n\n\nAdding And Editing Accounts\n---------------------------\n\nYou add new accounts using the *add* command::\n\n > avendesora add [<template>]\n\nThe available templates can be found using::\n\n > avendesora help add\n\nYou can add new templates or edit the existing templates by changing \n*account_templates* in ~/.config/avendesora/config.\n\nThe *add* command will open your editor (set this with the *edit_template* \nsetting in the config file). If you are using default version of *edit_template* \nthe template will be opened in Vim with the *n* key is mapped to take you to the \nnext field. You can edit any part of the template you like, but at a minimum you \nneed to edit the fields.\n\nOnce an account exists, you can edit it using::\n\n > avendesora edit [<account>]\n\nThis opens the accounts file with your editor (set this with the *edit_account* \nsetting in the config file). If you are using default version of *edit_account*, \nwhich uses VIM, it should take you directly to the account.\n\n\nFinding Accounts\n----------------\n\nThere are two ways of finding accounts. First, you can list any accounts whose \nname or aliases contains a text fragment. For example::\n\n > avendesora find bank\n bank:\n bankofamerica (boa)\n\nSecond, you can list any accounts that contain a text fragment in any non-secret \nfield. For example::\n\n > avendesora search 4408\n 4408:\n bankofamerica (boa)\n\n\nAutotyping Passwords\n--------------------\n\nThere are a couple of things that must be done to enable autotyping of \npasswords. First, at least some secrets must be configured for discovery. \nDiscovery allows secrets to determine whether they are good candidates for use \nin a particular situation based on the environment. The environment includes \nsuch things as with title of the active window, the user name, the host name, \netc. If multiple secrets are suitable, a small window pops up and lets you \nchoose between them. To see how to configure secrets for discovery, run \n'avendesora help discovery'.\n\nTo make secret discovery easier and more robust it is helpful to add a plugin to \nyour web browser to make its title more informative. For Firefox, the best \nplugin to use is *AddURLToWindowTitle*. For Chrome it is *URLinTitle*. (The \nlatest versions of Firefox are incompatible with *AddURLToWindowTitle*, however \nyou can use the Firefox version of *URLinTitle* instead.) It is recommended that \nyou install the appropriate one into your browser. For AddURLToWindowTitle, set \nthe following options:\n\n | show full URL = yes\n | separator string = '-'\n | show field attributes = no\n\nFor URLinTitle, set:\n\n | tab title format = '{title} - {protocol}://{hostname}{port}/{path}'\n\nFinally, you need to configure your window manager to run Avendesora when you \ntype a special hot key, such as ``Alt p``. The idea is that you are in \na situation where you need a secret, such as visiting your bank's website in \nyour browser, then you click on the account name field with your mouse and type \nyour hot key. This runs Avendesora without an account name. In this case, \nAvendesora uses secret discovery to determine which secret to use and the script \nthat should be used to produce the required information. Generally the script \nwould be to enter the account name, then tab, then the password, and finally \nreturn, but you can configure the script as you choose. This is all done as part \nof configuring discovery. The method for associating Avendesora to a particular \nhot key is dependent on your window manager. With Gnome, it requires that you \nopen your Keyboard Shortcuts preferences and create a new shortcut. When you do \nthis, choose 'avendesora value' as the command to run.\n\n\nPython API\n----------\n\nYou can access account information from Avendesora using Python using a simple \nrelatively high-level interface as shown in this example:\n\n.. code-block:: python\n\n from avendesora import PasswordGenerator, PasswordError\n from inform import display, fatal, os_error\n from shlib import Run\n from pathlib import Path\n\n try:\n pw = PasswordGenerator()\n account = pw.get_account('mybank')\n name = account.get_value('NAME')\n username = account.get_value('username')\n passcode = account.get_value('passcode')\n url = account.get_value('ofxurl')\n except PasswordError as err:\n fatal(err)\n\n try:\n curl = Run(f'curl --user {username!s}:{passcode!s} {url!s}', 'sOEW0')\n Path(f'{name!s}.ofx').write_text(curl.stdout)\n except OSError as err:\n fatal(os_error(err))\n\n\nPasswordGenerator():\n Initializes the password generator. You should pass no arguments.\n\nget_account(name, request_seed=False, stealth_name=None):\n Accesses a particular account. Takes a string for the account name or alias. \n The name is case insensitive and the '-' may be given for '_'.\n\n Optionally takes a second argument (*request_seed*) that may be a Boolean, \n a string, or a function that returns a string. The string is used as an \n additional seed (see: `avendesora help misdirection`), and if True is passed \n in, the user in queried for the seed.\n\n The stealth name is used as account name if the account is a stealth \n account.\n\n\nget_name():\n return name of account.\n\nget_value(field):\n Returns the value of a particular account attribute given a user-oriented \n string that describes the desired attribute. The value requested must be \n a scalar value, meaning that you must individually request members of arrays \n or dictionary attributes. Here are some examples that demonstrate the various \n ways of accessing the various kinds of attributes:\n\n .. code-block:: python\n\n passcode = account.get_value()\n username = account.get_value('username')\n both = account.get_value('username: {username}, password: {passcode}')\n checking = account.get_value('accounts.checking')\n savings = account.get_value('accounts[checking]')\n answer0 = account.get_value(0)\n answer1 = account.get_value('questions.1')\n answer2 = account.get_value('questions[2]')\n\n If the argument passed to get_value is a field, then it may consist of \n a name (the identifier for the first level of the field) and a key (the \n identifier for the second level of the field). The field is case insensitive \n and a '-' will match a '_' and visa versa.\n\n You can also specify the name and key separately in a tuple:\n\n .. code-block:: python\n\n username = account.get_value(('username',))\n checking = account.get_value(('accounts', 'checking'))\n answer0 = account.get_value((0,))\n answer1 = account.get_value(('questions', 1))\n\n The value is returned as an object that contains four attributes, value (the \n actual value), is_secret (whether the value is secret or contains a secret), \n name (the name of the value), and desc (the description, contains the actual \n question of the answer to a question is requested). Converting the object \n to a string returns the value rendered as a string. There is also the \n render() method that returns a string that combines the name and the \n description with the value. It takes an optional collection of format \n strings, the first one that matches is used. The format strings may contain \n keys in braces that get replaced by the corresponding attributes. The known \n keys are n {name}, k (key), f (field, combination of name and key), \n d (description) and v (value). A format string does not match it if \n contains a key for a value that is not available. If no format string \n matches, the value is returned as a string. The default formats are ('{f} \n ({d}): {v}', '{f}: {v}').\n\n If a composite field is requested get_value() raises a PasswordError, and \n the exception contains the *is_collection* and *collection* attributes. The \n first is a Boolean and the second is the list of available keys. \n PassworError returns None for unknown attributes, so it is always safe to \n access these attributes without checking whether they exist.\n\nget_values(field):\n Used to get the values for a composite field. It iterates through the value \n and returns a tuple that contains the key and the value for each item in the \n field.\n\n Field is an identifier that may consist of a name (the identifier for the \n first level of the field) and a key (the identifier for the second level of \n the field). The field is case insensitive and a '-' will match a '_' and \n visa versa.\n\n Here is how you might iterate through both the scalar and composite values \n in an account:\n\n .. code-block:: python\n\n try:\n value = acct.get_value(field)\n lines += value.render('{n}: {v}').split('\\n')\n except PasswordError as e:\n if not e.is_collection:\n raise\n lines += [name + ':']\n for key, value in acct.get_values(name):\n lines += indent(\n value.render(('{k}) {d}: {v}', '{k}: {v}'))\n ).split('\\n')\n\nget_fields():\n Iterates through the fields, each iteration yields a name and possibly \n a collection of keys ([None] is returned if the name corresponds to \n a scalar). The name and keys returned are the resolved names, which can be \n passed to get_scalar() and get_composite().\n\n Here is how this method can be used to iterate through the account values:\n\n .. code-block:: python\n\n # gather user fields\n lines = []\n for field, keys in account.get_fields():\n if keys == [None]:\n v = account.get_value(field)\n lines += v.render('{n}: {v}').split('\\n')\n else:\n lines.append(field + ':')\n for k, v in account.get_values(field):\n lines += indent(\n v.render(('{k}) {d}: {v}', '{k}: {v}'))\n ).split('\\n')\n account_summary = '\\n'.join(lines)\n\n get_fields() accepts a Boolean argument that if specified and is true will \n iterate through all fields, including those generally only used by \n Avendesora, such as aliases and discovery.\n\n\nget_scalar(name, key=None, default=False):\n A lower level interface than get_value that given a name and perhaps a key \n returns a scalar value. Also takes an optional default value that is \n returned if the value is not found. Unlike get_value, the actual value is \n returned, not a object that contains multiple facets of the value. Also, the \n name and key must match exactly.\n\n The name is the field name, and the key would identity which value is \n desired if the field is a composite. If default is False, an error is raise \n if the value is not present, otherwise the default value itself is returned.\n\n If the value returned is an Avendesora object (GeneratedSecret,\n ObscuredSecret, Script), then you should cast it to a string to get its\n resolved value.\n\nget_composite(name):\n A lower level interface than get_value that given a name returns the value \n of the associated field, which may be a scalar (string or integer) or \n a composite (array of dictionary). Unlike get_value, the actual value is \n returned, not a object that contains multiple facets of the value. Also, \n the name and key must match exactly.\n\n If the value returned is an Avendesora object (GeneratedSecret,\n ObscuredSecret, Script), then you should cast it to a string to get its\n resolved value.\n\nAPI Example\n-----------\n\nThe following example creates encrypted files that contain account information \nthat would be needed by close family members and by a business partner in case \nanything happened to you. This is an abbreviated version of an example given in \nthe users' guide.\n\n.. code-block:: python\n\n #!/bin/env python3\n\n from avendesora import PasswordGenerator, PasswordError\n from textwrap import dedent\n from inform import (\n display, done, Error, error, indent, is_collection, os_error\n )\n import gnupg\n\n\n files = [\n { 'FILENAME': 'family.gpg',\n 'RECIPIENTS': 'me@home.com son@home.com daughter@home.com'.split(),\n 'ACCOUNTS': 'bank brokerage creditcard'.split(),\n },\n { 'FILENAME': 'partner.gpg',\n 'RECIPIENTS': 'me@work.com partner@work.com'.split(),\n 'ACCOUNTS': 'login ssh root backups'.split(),\n },\n ]\n\n try:\n pw = PasswordGenerator()\n\n for each in files:\n accounts = []\n for account_name in each['ACCOUNTS']:\n acct = pw.get_account(account_name)\n title = acct.get_scalar('desc', default=account_name)\n lines = [title, len(title)*'=']\n\n for name, keys in acct.get_fields():\n if keys:\n lines.append(name + ':')\n for key, value in acct.get_values(name):\n lines += indent(\n value.render(('{k}) {d}: {v}', '{k}: {v}'))\n ).split('\\n')\n else:\n value = acct.get_value(name)\n lines += value.render('{n}: {v}').split('\\n')\n accounts.append('\\n'.join(lines))\n\n gpg = gnupg.GPG(gpgbinary='gpg2')\n encrypted = gpg.encrypt('\\n\\n\\n'.join(accounts), each['RECIPIENTS'])\n if not encrypted.ok:\n raise Error(\n 'unable to encrypt:', encrypted.stderr, culprit=each['FILENAME']\n )\n try:\n with open(each['FILENAME'], 'w') as file:\n file.write(str(encrypted))\n print(\"%s: created.\" % each['FILENAME'])\n except OSError as e:\n raise Error(os_error(e))\n\n except (PasswordError, Error) as e:\n e.terminate()\n\n\nGetting Help\n------------\n\nYou can find the documentation on `ReadTheDocs <https://avendesora.readthedocs.io>`_.\n\nThe *help* command provides information on how to use Avendesora's various \nfeatures. To get a listing of the topics available, use::\n\n avendesora help\n\nThen, for information on a specific topic use::\n\n avendesora help <topic>\n\nIt is worth browsing all of the available topics at least once to get a sense of \nall that Avendesora can do.\n\n\nContributing\n------------\n\nPlease ask questions or report bugs on `Github Issues \n<https://github.com/KenKundert/avendesora/issues>`_. I will entertain pull \nrequests if you make improvements. Currently *Avendesora* is very *Fedora* and \n*VIM* centric. I am particularly interested in help adapting *Avendesora* in the \nfollowing ways:\n\n- Support for other editors, window managers and distributions.\n- Support for Windows and OSX.\n- Support for Android and iOS (perhaps through exports to a password manager \n that already support smartphones).\n\n",
"bugtrack_url": null,
"license": null,
"summary": "A password generator and account manager.",
"version": "1.26",
"project_urls": {
"changelog": "https://avendesora.readthedocs.io/en/latest/releases.html",
"documentation": "https://avendesora.readthedocs.io",
"homepage": "https://avendesora.readthedocs.io",
"repository": "https://github.com/kenkundert/avendesora"
},
"split_keywords": [
"avendesora",
" password",
" password vault",
" xkcd"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "b1e766728d82beeb19720c2437a5e87f3a5fe7117022d6dba3fc944610ffe25c",
"md5": "2856796ad71c4d5bee12bde6e199963c",
"sha256": "144aedd6f6181382f13317969985bdb5a5d6f18774e43a3e1b4eafefa73b9b49"
},
"downloads": -1,
"filename": "avendesora-1.26-py3-none-any.whl",
"has_sig": false,
"md5_digest": "2856796ad71c4d5bee12bde6e199963c",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.6",
"size": 174190,
"upload_time": "2024-08-29T02:26:22",
"upload_time_iso_8601": "2024-08-29T02:26:22.744231Z",
"url": "https://files.pythonhosted.org/packages/b1/e7/66728d82beeb19720c2437a5e87f3a5fe7117022d6dba3fc944610ffe25c/avendesora-1.26-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "cc8e73f41fc976ca77e93dbbbbd34552f531daa3a610193eae36da1f3e469ced",
"md5": "1a074ce666d17264e95bbc3d05480a8c",
"sha256": "be2645bcac575be8974878238fe45eb80a159445744b6e69d342d9bcd85293dd"
},
"downloads": -1,
"filename": "avendesora-1.26.tar.gz",
"has_sig": false,
"md5_digest": "1a074ce666d17264e95bbc3d05480a8c",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.6",
"size": 167792,
"upload_time": "2024-08-29T02:26:25",
"upload_time_iso_8601": "2024-08-29T02:26:25.023852Z",
"url": "https://files.pythonhosted.org/packages/cc/8e/73f41fc976ca77e93dbbbbd34552f531daa3a610193eae36da1f3e469ced/avendesora-1.26.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-08-29 02:26:25",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "kenkundert",
"github_project": "avendesora",
"travis_ci": true,
"coveralls": true,
"github_actions": true,
"tox": true,
"lcname": "avendesora"
}