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 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
@@ -0,0 +1,2 @@
1
+ # sfnx-terminal
2
+ A minimal terminal password manager that uses Typer, SQLModel, Argon2 Key Derivation, AES and SQLite
sfnx-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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}")
@@ -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,2 @@
1
+ [console_scripts]
2
+ sfnx = sfnx.main:app
@@ -0,0 +1,5 @@
1
+ typer[all]
2
+ rich
3
+ cryptography
4
+ argon2-cffi
5
+ sqlmodel
@@ -0,0 +1 @@
1
+ sfnx