sfnx 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sfnx-0.1.0/LICENSE +7 -0
- sfnx-0.1.0/PKG-INFO +20 -0
- sfnx-0.1.0/README.md +2 -0
- sfnx-0.1.0/setup.cfg +4 -0
- sfnx-0.1.0/setup.py +30 -0
- sfnx-0.1.0/sfnx/__init__.py +0 -0
- sfnx-0.1.0/sfnx/db.py +174 -0
- sfnx-0.1.0/sfnx/main.py +95 -0
- sfnx-0.1.0/sfnx/security.py +60 -0
- sfnx-0.1.0/sfnx.egg-info/PKG-INFO +20 -0
- sfnx-0.1.0/sfnx.egg-info/SOURCES.txt +13 -0
- sfnx-0.1.0/sfnx.egg-info/dependency_links.txt +1 -0
- sfnx-0.1.0/sfnx.egg-info/entry_points.txt +2 -0
- sfnx-0.1.0/sfnx.egg-info/requires.txt +5 -0
- sfnx-0.1.0/sfnx.egg-info/top_level.txt +1 -0
sfnx-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2024 Mohit Nair
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
sfnx-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: sfnx
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Classifier: Programming Language :: Python :: 3
|
|
5
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
6
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.7
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: typer[all]
|
|
14
|
+
Requires-Dist: rich
|
|
15
|
+
Requires-Dist: cryptography
|
|
16
|
+
Requires-Dist: argon2-cffi
|
|
17
|
+
Requires-Dist: sqlmodel
|
|
18
|
+
|
|
19
|
+
# sfnx-terminal
|
|
20
|
+
A minimal terminal password manager that uses Typer, SQLModel, Argon2 Key Derivation, AES and SQLite
|
sfnx-0.1.0/README.md
ADDED
sfnx-0.1.0/setup.cfg
ADDED
sfnx-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="sfnx",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
packages=find_packages(),
|
|
7
|
+
install_requires=[
|
|
8
|
+
"typer[all]",
|
|
9
|
+
"rich",
|
|
10
|
+
"cryptography",
|
|
11
|
+
"argon2-cffi",
|
|
12
|
+
"sqlmodel"
|
|
13
|
+
],
|
|
14
|
+
entry_points={
|
|
15
|
+
"console_scripts": [
|
|
16
|
+
"sfnx=sfnx.main:app",
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
python_requires='>=3.7', # Specify Python version requirements
|
|
20
|
+
classifiers=[
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.7",
|
|
23
|
+
"Programming Language :: Python :: 3.8",
|
|
24
|
+
"Programming Language :: Python :: 3.9",
|
|
25
|
+
"License :: OSI Approved :: MIT License",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
],
|
|
28
|
+
long_description=open('README.md').read(),
|
|
29
|
+
long_description_content_type='text/markdown',
|
|
30
|
+
)
|
|
File without changes
|
sfnx-0.1.0/sfnx/db.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from sqlmodel import SQLModel, create_engine, Session, select, Field, UniqueConstraint
|
|
2
|
+
from sfnx.security import derive_key, encrypt, decrypt
|
|
3
|
+
from sqlmodel import SQLModel, Field
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
db_file = "sfnx.db"
|
|
9
|
+
db_url = f"sqlite:///{db_file}"
|
|
10
|
+
|
|
11
|
+
engine = create_engine(db_url, echo=False)
|
|
12
|
+
|
|
13
|
+
def init_db():
|
|
14
|
+
try:
|
|
15
|
+
SQLModel.metadata.create_all(engine)
|
|
16
|
+
except Exception as e:
|
|
17
|
+
print(f"Error initializing the database: {e}")
|
|
18
|
+
sys.exit(1)
|
|
19
|
+
|
|
20
|
+
class Secrets(SQLModel, table=True):
|
|
21
|
+
service: str = Field(nullable=False, max_length=64, primary_key=True)
|
|
22
|
+
username: str = Field(nullable=False, max_length=64, primary_key=True)
|
|
23
|
+
password: bytes = Field(nullable=False, max_length=255)
|
|
24
|
+
salt: bytes = Field(nullable=False)
|
|
25
|
+
|
|
26
|
+
def configure(master_password: str, verification_secret: str) -> bytes:
|
|
27
|
+
try:
|
|
28
|
+
init_db()
|
|
29
|
+
salt = os.urandom(16)
|
|
30
|
+
reference = encrypt(derive_key(master_password, salt), verification_secret)
|
|
31
|
+
configuration = Secrets(
|
|
32
|
+
service="sfnx_secret",
|
|
33
|
+
username=verification_secret,
|
|
34
|
+
password=reference,
|
|
35
|
+
salt=salt
|
|
36
|
+
)
|
|
37
|
+
with Session(engine) as session:
|
|
38
|
+
session.add(configuration)
|
|
39
|
+
session.commit()
|
|
40
|
+
except Exception as e:
|
|
41
|
+
print(f"Error during configuration: {e}")
|
|
42
|
+
sys.exit(1)
|
|
43
|
+
|
|
44
|
+
def check_exists() -> bool:
|
|
45
|
+
try:
|
|
46
|
+
with Session(engine) as session:
|
|
47
|
+
statement = select(Secrets).where(Secrets.service == "sfnx_secret")
|
|
48
|
+
result = session.exec(statement).first()
|
|
49
|
+
return result is not None
|
|
50
|
+
except Exception as e:
|
|
51
|
+
print(f"Error checking existence: {e}")
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
def check_db_exists():
|
|
55
|
+
db_path = "sfnx.db"
|
|
56
|
+
return os.path.isfile(db_path)
|
|
57
|
+
|
|
58
|
+
def verify_user_master_password(master_password_attempt: str) -> bool:
|
|
59
|
+
try:
|
|
60
|
+
with Session(engine) as session:
|
|
61
|
+
statement = select(Secrets).where(Secrets.service == "sfnx_secret")
|
|
62
|
+
result = session.exec(statement).first()
|
|
63
|
+
|
|
64
|
+
if result is None:
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
encrypted_secret = getattr(result, "password")
|
|
68
|
+
verification_secret = getattr(result, "username")
|
|
69
|
+
salt = getattr(result, "salt")
|
|
70
|
+
key = derive_key(master_password_attempt, salt)
|
|
71
|
+
try:
|
|
72
|
+
decrypted_secret = decrypt(key, encrypted_secret)
|
|
73
|
+
except ValueError:
|
|
74
|
+
print("Wrong master password.")
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
return decrypted_secret == verification_secret
|
|
78
|
+
except Exception as e:
|
|
79
|
+
print(f"Error verifying master password: {e}")
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def get_user_name(master_password_attempt: str) -> str:
|
|
83
|
+
try:
|
|
84
|
+
with Session(engine) as session:
|
|
85
|
+
if check_exists():
|
|
86
|
+
statement = select(Secrets).where(Secrets.service == "sfnx_secret")
|
|
87
|
+
result = session.exec(statement).first()
|
|
88
|
+
|
|
89
|
+
verification_secret = getattr(result, "username")
|
|
90
|
+
encrypted_secret = getattr(result, "password")
|
|
91
|
+
salt = getattr(result, "salt")
|
|
92
|
+
key = derive_key(master_password_attempt, salt)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
decrypted_secret = decrypt(key, encrypted_secret)
|
|
96
|
+
except ValueError:
|
|
97
|
+
return ""
|
|
98
|
+
|
|
99
|
+
if decrypted_secret == verification_secret:
|
|
100
|
+
return decrypted_secret
|
|
101
|
+
else:
|
|
102
|
+
return ""
|
|
103
|
+
except Exception as e:
|
|
104
|
+
print(f"Error retrieving user name: {e}")
|
|
105
|
+
return ""
|
|
106
|
+
|
|
107
|
+
def check_if_service_and_uname_already_exist(service: str, username: Optional[str]) -> bool:
|
|
108
|
+
try:
|
|
109
|
+
with Session(engine) as session:
|
|
110
|
+
statement = select(Secrets).where(Secrets.username == username).where(Secrets.service == service)
|
|
111
|
+
result = session.exec(statement).first()
|
|
112
|
+
return result is not None
|
|
113
|
+
except Exception as e:
|
|
114
|
+
print(f"Error checking service and username existence: {e}")
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
def add_password(master_password_attempt: str, service: str, username: Optional[str], password: str):
|
|
118
|
+
try:
|
|
119
|
+
if verify_user_master_password(master_password_attempt) and not service == "sfnx_secret":
|
|
120
|
+
with Session(engine) as session:
|
|
121
|
+
if not check_if_service_and_uname_already_exist(service, username):
|
|
122
|
+
salt = os.urandom(16)
|
|
123
|
+
key = derive_key(master_password_attempt, salt)
|
|
124
|
+
s_password = encrypt(key, password)
|
|
125
|
+
secret = Secrets(
|
|
126
|
+
service=service,
|
|
127
|
+
username=username,
|
|
128
|
+
password=s_password,
|
|
129
|
+
salt=salt
|
|
130
|
+
)
|
|
131
|
+
session.add(secret)
|
|
132
|
+
session.commit()
|
|
133
|
+
print("Password added successfully!")
|
|
134
|
+
else:
|
|
135
|
+
print("Secrets associated with the same service and username already exist.")
|
|
136
|
+
return
|
|
137
|
+
except Exception as e:
|
|
138
|
+
print(f"Error adding password: {e}")
|
|
139
|
+
|
|
140
|
+
def delete_password(master_password_attempt: str, service: str, username: str):
|
|
141
|
+
try:
|
|
142
|
+
if verify_user_master_password(master_password_attempt):
|
|
143
|
+
with Session(engine) as session:
|
|
144
|
+
statement = select(Secrets).where(Secrets.service == service).where(Secrets.username == username)
|
|
145
|
+
result = session.exec(statement).first()
|
|
146
|
+
if result:
|
|
147
|
+
session.delete(result)
|
|
148
|
+
session.commit()
|
|
149
|
+
print("Password deleted successfully!")
|
|
150
|
+
except Exception as e:
|
|
151
|
+
print(f"Error deleting password: {e}")
|
|
152
|
+
|
|
153
|
+
def retrieve_password(master_password_attempt: str, service: str, username: str):
|
|
154
|
+
try:
|
|
155
|
+
if verify_user_master_password(master_password_attempt):
|
|
156
|
+
with Session(engine) as session:
|
|
157
|
+
statement = select(Secrets).where(Secrets.service == service).where(Secrets.username == username)
|
|
158
|
+
results = session.exec(statement).all()
|
|
159
|
+
|
|
160
|
+
if results:
|
|
161
|
+
for result in results:
|
|
162
|
+
username = result.username
|
|
163
|
+
key = derive_key(master_password_attempt, result.salt)
|
|
164
|
+
try:
|
|
165
|
+
password = decrypt(key, result.password)
|
|
166
|
+
except ValueError:
|
|
167
|
+
password = None
|
|
168
|
+
|
|
169
|
+
print(f"Password: {password}")
|
|
170
|
+
print("Password(s) retrieved successfully!")
|
|
171
|
+
else:
|
|
172
|
+
print("No records found for this service.")
|
|
173
|
+
except Exception as e:
|
|
174
|
+
print(f"Error retrieving password: {e}")
|
sfnx-0.1.0/sfnx/main.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from typer import Typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.text import Text
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
import getpass
|
|
6
|
+
import os
|
|
7
|
+
from sfnx.db import init_db, verify_user_master_password, check_db_exists, configure, get_user_name, add_password, retrieve_password, delete_password
|
|
8
|
+
from sfnx.security import encrypt, decrypt, derive_key
|
|
9
|
+
|
|
10
|
+
app = Typer()
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
rules = [
|
|
14
|
+
("Rule 1:", "The master password is the prime password which you will be using to access all your passwords in sfnx. Forgetting it means irreversibly losing access to all your other passwords. Always remember it."),
|
|
15
|
+
("Rule 2:", "Creating an easy-to-remember password doesn't mean making it short and weak. It is better to follow this for all passwords you store in this password manager."),
|
|
16
|
+
("Rule 3:", "Your master password must have at least 15 characters. It is recommended to use a long passphrase; for example: \"myc@tn@medg@mbitst@rtedthes0vietunion@ccident@lly\""),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
@app.command("init")
|
|
20
|
+
def init():
|
|
21
|
+
try:
|
|
22
|
+
if not check_db_exists():
|
|
23
|
+
rules_text = "\n".join([f"{rule} {point}" for rule, point in rules])
|
|
24
|
+
panel_content = Text(rules_text, style="cyan")
|
|
25
|
+
console.print(Panel(panel_content, title="Important Rules", expand=False))
|
|
26
|
+
|
|
27
|
+
master_password = getpass.getpass("Enter the master password you wish to use: ")
|
|
28
|
+
confirm = getpass.getpass("Enter the password again [confirm]: ")
|
|
29
|
+
|
|
30
|
+
if confirm != master_password:
|
|
31
|
+
console.print("[bold red]Error:[/bold red] Passwords do not match. Please try again.", style="bold red")
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
name = input("Enter your name or alias [required]: ")
|
|
35
|
+
console.print(f"\nThank you for setting up sfnx, {name}!", style="bold green")
|
|
36
|
+
salt = os.urandom(16)
|
|
37
|
+
key = derive_key(master_password, salt)
|
|
38
|
+
encrypted_secret = encrypt(key, name)
|
|
39
|
+
configure(master_password, name)
|
|
40
|
+
else:
|
|
41
|
+
print("This is a configuration test to see if you have setup your password manager properly. See sfnx --help for more details.")
|
|
42
|
+
master_password_attempt = getpass.getpass("Enter your master password: ")
|
|
43
|
+
if verify_user_master_password(master_password_attempt):
|
|
44
|
+
username = get_user_name(master_password_attempt)
|
|
45
|
+
print(f"Welcome, {username}!")
|
|
46
|
+
except Exception as e:
|
|
47
|
+
console.print(f"[bold red]Error:[/bold red] {e}", style="bold red")
|
|
48
|
+
|
|
49
|
+
@app.command("addpass")
|
|
50
|
+
def addpass():
|
|
51
|
+
try:
|
|
52
|
+
if not check_db_exists():
|
|
53
|
+
init()
|
|
54
|
+
else:
|
|
55
|
+
service = input("Enter the name of the service: ")
|
|
56
|
+
username = input("Enter the username used for the service: ")
|
|
57
|
+
password = getpass.getpass("Enter the password used for this service and username: ")
|
|
58
|
+
c_password = getpass.getpass("Enter the above password again for confirmation: ")
|
|
59
|
+
if password == c_password:
|
|
60
|
+
master_password_attempt = getpass.getpass("Enter your master password: ")
|
|
61
|
+
add_password(master_password_attempt, service, username, password)
|
|
62
|
+
else:
|
|
63
|
+
console.print("[bold red]Error:[/bold red] Passwords do not match. Please try again.", style="bold red")
|
|
64
|
+
return
|
|
65
|
+
except Exception as e:
|
|
66
|
+
console.print(f"[bold red]Error:[/bold red] {e}", style="bold red")
|
|
67
|
+
|
|
68
|
+
@app.command("delpass")
|
|
69
|
+
def delpass():
|
|
70
|
+
try:
|
|
71
|
+
if not check_db_exists():
|
|
72
|
+
init()
|
|
73
|
+
else:
|
|
74
|
+
service = input("Enter the name of the service: ")
|
|
75
|
+
username = input("Enter the username used for the service: ")
|
|
76
|
+
master_password_attempt = getpass.getpass("Enter your master password: ")
|
|
77
|
+
delete_password(master_password_attempt, service, username)
|
|
78
|
+
except Exception as e:
|
|
79
|
+
console.print(f"[bold red]Error:[/bold red] {e}", style="bold red")
|
|
80
|
+
|
|
81
|
+
@app.command("viewpass")
|
|
82
|
+
def viewpass():
|
|
83
|
+
try:
|
|
84
|
+
if not check_db_exists():
|
|
85
|
+
init()
|
|
86
|
+
else:
|
|
87
|
+
service = input("Enter the name of the service: ")
|
|
88
|
+
username = input("Enter the username used for the service: ")
|
|
89
|
+
master_password_attempt = getpass.getpass("Enter your master password: ")
|
|
90
|
+
retrieve_password(master_password_attempt, service, username)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
console.print(f"[bold red]Error:[/bold red] {e}", style="bold red")
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
app()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from argon2 import PasswordHasher
|
|
2
|
+
from argon2 import Type, low_level
|
|
3
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
4
|
+
from cryptography.hazmat.primitives import padding
|
|
5
|
+
from cryptography.hazmat.backends import default_backend
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
def derive_key(m_password: str, salt: bytes) -> bytes:
|
|
9
|
+
try:
|
|
10
|
+
key = low_level.hash_secret_raw(
|
|
11
|
+
m_password.encode(),
|
|
12
|
+
salt,
|
|
13
|
+
time_cost=2,
|
|
14
|
+
memory_cost=102400,
|
|
15
|
+
parallelism=8,
|
|
16
|
+
hash_len=32,
|
|
17
|
+
type=Type.ID
|
|
18
|
+
)
|
|
19
|
+
return key
|
|
20
|
+
except Exception as e:
|
|
21
|
+
raise RuntimeError("Error deriving key.")
|
|
22
|
+
|
|
23
|
+
def encrypt(key: bytes, plaintext: str) -> bytes:
|
|
24
|
+
try:
|
|
25
|
+
if len(key) != 32:
|
|
26
|
+
raise ValueError("Key must be 32 bytes long")
|
|
27
|
+
|
|
28
|
+
iv = os.urandom(16)
|
|
29
|
+
|
|
30
|
+
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
|
|
31
|
+
encryptor = cipher.encryptor()
|
|
32
|
+
|
|
33
|
+
padder = padding.PKCS7(algorithms.AES.block_size).padder()
|
|
34
|
+
padded_data = padder.update(plaintext.encode()) + padder.finalize()
|
|
35
|
+
|
|
36
|
+
encrypted_data = encryptor.update(padded_data) + encryptor.finalize()
|
|
37
|
+
|
|
38
|
+
return iv + encrypted_data
|
|
39
|
+
except Exception as e:
|
|
40
|
+
raise RuntimeError("Error encrypting data.")
|
|
41
|
+
|
|
42
|
+
def decrypt(key: bytes, encrypted_data: bytes) -> str:
|
|
43
|
+
try:
|
|
44
|
+
if len(key) != 32:
|
|
45
|
+
raise ValueError("Key must be 32 bytes long")
|
|
46
|
+
|
|
47
|
+
iv = encrypted_data[:16]
|
|
48
|
+
encrypted_data = encrypted_data[16:]
|
|
49
|
+
|
|
50
|
+
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
|
|
51
|
+
decryptor = cipher.decryptor()
|
|
52
|
+
|
|
53
|
+
padded_plaintext = decryptor.update(encrypted_data) + decryptor.finalize()
|
|
54
|
+
|
|
55
|
+
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
|
|
56
|
+
plaintext = unpadder.update(padded_plaintext) + unpadder.finalize()
|
|
57
|
+
|
|
58
|
+
return plaintext.decode()
|
|
59
|
+
except Exception as e:
|
|
60
|
+
raise RuntimeError("Error decrypting data.")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: sfnx
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Classifier: Programming Language :: Python :: 3
|
|
5
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
6
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.7
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: typer[all]
|
|
14
|
+
Requires-Dist: rich
|
|
15
|
+
Requires-Dist: cryptography
|
|
16
|
+
Requires-Dist: argon2-cffi
|
|
17
|
+
Requires-Dist: sqlmodel
|
|
18
|
+
|
|
19
|
+
# sfnx-terminal
|
|
20
|
+
A minimal terminal password manager that uses Typer, SQLModel, Argon2 Key Derivation, AES and SQLite
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
sfnx/__init__.py
|
|
5
|
+
sfnx/db.py
|
|
6
|
+
sfnx/main.py
|
|
7
|
+
sfnx/security.py
|
|
8
|
+
sfnx.egg-info/PKG-INFO
|
|
9
|
+
sfnx.egg-info/SOURCES.txt
|
|
10
|
+
sfnx.egg-info/dependency_links.txt
|
|
11
|
+
sfnx.egg-info/entry_points.txt
|
|
12
|
+
sfnx.egg-info/requires.txt
|
|
13
|
+
sfnx.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sfnx
|