pitt 0.4.7__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.
- pitt-0.4.7/PKG-INFO +84 -0
- pitt-0.4.7/README.md +70 -0
- pitt-0.4.7/pyproject.toml +26 -0
- pitt-0.4.7/src/pitt/__init__.py +0 -0
- pitt-0.4.7/src/pitt/db_handler.py +118 -0
- pitt-0.4.7/src/pitt/functions.py +275 -0
- pitt-0.4.7/src/pitt/main.py +16 -0
- pitt-0.4.7/src/pitt/parse.py +30 -0
- pitt-0.4.7/src/pitt/security.py +59 -0
- pitt-0.4.7/src/pitt/utils.py +79 -0
pitt-0.4.7/PKG-INFO
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pitt
|
|
3
|
+
Version: 0.4.7
|
|
4
|
+
Summary: an extremely simple terminal-based password manager
|
|
5
|
+
Author: quandelaa
|
|
6
|
+
Author-email: quandelaa <sergiotobamasilalahi@gmail.com>
|
|
7
|
+
Requires-Dist: argon2-cffi>=25.1.0
|
|
8
|
+
Requires-Dist: cryptography>=49.0.0
|
|
9
|
+
Requires-Dist: platformdirs>=4.10.1
|
|
10
|
+
Requires-Dist: pyperclip>=1.11.0
|
|
11
|
+
Requires-Dist: rich>=15.0.0
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# pitt
|
|
16
|
+
|
|
17
|
+
an extremely simple terminal-based password manager.
|
|
18
|
+
|
|
19
|
+
## installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install git+https://github.com/quandelaa/pitt.git
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## usage
|
|
26
|
+
|
|
27
|
+
### commands
|
|
28
|
+
|
|
29
|
+
1. `pitt init`:
|
|
30
|
+
* asks for a master password
|
|
31
|
+
* when given, encrypts it using Argon2id and stores it in the database
|
|
32
|
+
|
|
33
|
+
example:
|
|
34
|
+
```bash
|
|
35
|
+
pitt init
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
2. `pitt add`:
|
|
39
|
+
* create random password
|
|
40
|
+
* encrypts the random password using Fernet
|
|
41
|
+
* stores the encrypted password with a specified username or service or note in the database
|
|
42
|
+
|
|
43
|
+
example:
|
|
44
|
+
```bash
|
|
45
|
+
pitt add --service github --username quandelaa --note "this is my song"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
3. `pitt get`:
|
|
49
|
+
* gets the password that is saved with the given service or username
|
|
50
|
+
* decrypts the password
|
|
51
|
+
* copies it to user's clipboard
|
|
52
|
+
|
|
53
|
+
example:
|
|
54
|
+
```bash
|
|
55
|
+
pitt get --service github --username quandelaa
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
4. `pitt list`:
|
|
59
|
+
* lists only the details (service, username and notes) of all the saved passwords
|
|
60
|
+
|
|
61
|
+
example:
|
|
62
|
+
```bash
|
|
63
|
+
pitt list"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
4. `pitt del`:
|
|
67
|
+
* deletes a password in the password vault that is saved with the given service or username
|
|
68
|
+
|
|
69
|
+
example:
|
|
70
|
+
```bash
|
|
71
|
+
pitt del --service github --username quandelaa
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## license
|
|
75
|
+
|
|
76
|
+
this project is licensed under the MIT License - see the LICENSE file for details
|
|
77
|
+
|
|
78
|
+
## credits
|
|
79
|
+
|
|
80
|
+
inspired by: themohitnair's sfnx | (https://github.com/themohitnair/sfnx)
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
curated by me with passion and a teensy bit of stress
|
pitt-0.4.7/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# pitt
|
|
2
|
+
|
|
3
|
+
an extremely simple terminal-based password manager.
|
|
4
|
+
|
|
5
|
+
## installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install git+https://github.com/quandelaa/pitt.git
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## usage
|
|
12
|
+
|
|
13
|
+
### commands
|
|
14
|
+
|
|
15
|
+
1. `pitt init`:
|
|
16
|
+
* asks for a master password
|
|
17
|
+
* when given, encrypts it using Argon2id and stores it in the database
|
|
18
|
+
|
|
19
|
+
example:
|
|
20
|
+
```bash
|
|
21
|
+
pitt init
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
2. `pitt add`:
|
|
25
|
+
* create random password
|
|
26
|
+
* encrypts the random password using Fernet
|
|
27
|
+
* stores the encrypted password with a specified username or service or note in the database
|
|
28
|
+
|
|
29
|
+
example:
|
|
30
|
+
```bash
|
|
31
|
+
pitt add --service github --username quandelaa --note "this is my song"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
3. `pitt get`:
|
|
35
|
+
* gets the password that is saved with the given service or username
|
|
36
|
+
* decrypts the password
|
|
37
|
+
* copies it to user's clipboard
|
|
38
|
+
|
|
39
|
+
example:
|
|
40
|
+
```bash
|
|
41
|
+
pitt get --service github --username quandelaa
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
4. `pitt list`:
|
|
45
|
+
* lists only the details (service, username and notes) of all the saved passwords
|
|
46
|
+
|
|
47
|
+
example:
|
|
48
|
+
```bash
|
|
49
|
+
pitt list"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
4. `pitt del`:
|
|
53
|
+
* deletes a password in the password vault that is saved with the given service or username
|
|
54
|
+
|
|
55
|
+
example:
|
|
56
|
+
```bash
|
|
57
|
+
pitt del --service github --username quandelaa
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## license
|
|
61
|
+
|
|
62
|
+
this project is licensed under the MIT License - see the LICENSE file for details
|
|
63
|
+
|
|
64
|
+
## credits
|
|
65
|
+
|
|
66
|
+
inspired by: themohitnair's sfnx | (https://github.com/themohitnair/sfnx)
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
curated by me with passion and a teensy bit of stress
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pitt"
|
|
3
|
+
version = "0.4.7"
|
|
4
|
+
description = "an extremely simple terminal-based password manager"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "quandelaa", email = "sergiotobamasilalahi@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"argon2-cffi>=25.1.0",
|
|
12
|
+
"cryptography>=49.0.0",
|
|
13
|
+
"platformdirs>=4.10.1",
|
|
14
|
+
"pyperclip>=1.11.0",
|
|
15
|
+
"rich>=15.0.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
pitt = "pitt.main:main"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["uv_build>=0.11.26,<0.12.0"]
|
|
23
|
+
build-backend = "uv_build"
|
|
24
|
+
|
|
25
|
+
[dependency-groups]
|
|
26
|
+
dev = []
|
|
File without changes
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import sqlite3 as sql
|
|
2
|
+
from .utils import get_db_path
|
|
3
|
+
|
|
4
|
+
def init_db() -> None:
|
|
5
|
+
"""
|
|
6
|
+
Initialize the database that will store the encrypted passwords.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
db_path = str(get_db_path())
|
|
10
|
+
|
|
11
|
+
conn = sql.connect(db_path)
|
|
12
|
+
cur = conn.cursor()
|
|
13
|
+
|
|
14
|
+
cur.executescript(
|
|
15
|
+
"""CREATE TABLE IF NOT EXISTS passwords (
|
|
16
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
17
|
+
service TEXT,
|
|
18
|
+
username TEXT,
|
|
19
|
+
note TEXT,
|
|
20
|
+
password BLOB NOT NULL);"""
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
cur.executescript(
|
|
24
|
+
"""CREATE TABLE IF NOT EXISTS master (
|
|
25
|
+
master_hash TEXT NOT NULL,
|
|
26
|
+
salt BLOB NOT NULL);"""
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
conn.commit()
|
|
30
|
+
conn.close()
|
|
31
|
+
|
|
32
|
+
def configure_vault(salt: bytes, new_hash: str) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Configures the current master password hash stored in the passwords database
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
db_path = str(get_db_path())
|
|
38
|
+
|
|
39
|
+
conn = sql.connect(db_path)
|
|
40
|
+
cur = conn.cursor()
|
|
41
|
+
|
|
42
|
+
cur.execute("SELECT COUNT(*) FROM master")
|
|
43
|
+
row_count = cur.fetchone()[0]
|
|
44
|
+
|
|
45
|
+
if row_count == 0:
|
|
46
|
+
cur.execute("INSERT INTO master (salt, master_hash) VALUES ('', '')")
|
|
47
|
+
|
|
48
|
+
cur.execute("UPDATE master SET (master_hash, salt) = (?, ?)", (new_hash, salt))
|
|
49
|
+
|
|
50
|
+
conn.commit()
|
|
51
|
+
conn.close()
|
|
52
|
+
|
|
53
|
+
def store_password(service: str | None, username: str | None, note: str | None, password: bytes) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Stores the password inside of the created database
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
db_path = str(get_db_path())
|
|
59
|
+
|
|
60
|
+
conn = sql.connect(db_path)
|
|
61
|
+
cur = conn.cursor()
|
|
62
|
+
|
|
63
|
+
cur.execute("INSERT INTO passwords (service, username, note, password) VALUES (?, ?, ?, ?)", (service, username, note, password))
|
|
64
|
+
|
|
65
|
+
conn.commit()
|
|
66
|
+
conn.close()
|
|
67
|
+
|
|
68
|
+
def get_by_properties(service: str | None, username: str | None) -> list:
|
|
69
|
+
"""
|
|
70
|
+
Gets the encrypted password in the passwords database based on given service and username
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
db_path = str(get_db_path())
|
|
74
|
+
|
|
75
|
+
conn = sql.connect(db_path)
|
|
76
|
+
cur = conn.cursor()
|
|
77
|
+
|
|
78
|
+
if username is None and service is not None:
|
|
79
|
+
cur.execute("SELECT * FROM passwords WHERE service = ?", (service,))
|
|
80
|
+
elif username is not None and service is None:
|
|
81
|
+
cur.execute("SELECT * FROM passwords WHERE username = ?", (username,))
|
|
82
|
+
elif username is not None and service is not None:
|
|
83
|
+
cur.execute("SELECT * FROM passwords WHERE (service, username) = (?, ?)", (service, username))
|
|
84
|
+
|
|
85
|
+
results = cur.fetchall()
|
|
86
|
+
|
|
87
|
+
return results
|
|
88
|
+
|
|
89
|
+
def get_all() -> list:
|
|
90
|
+
"""
|
|
91
|
+
Gets the encrypted password in the passwords database based on given service and username
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
db_path = str(get_db_path())
|
|
95
|
+
|
|
96
|
+
conn = sql.connect(db_path)
|
|
97
|
+
cur = conn.cursor()
|
|
98
|
+
|
|
99
|
+
cur.execute("SELECT * FROM passwords")
|
|
100
|
+
|
|
101
|
+
results = cur.fetchall()
|
|
102
|
+
|
|
103
|
+
return results
|
|
104
|
+
|
|
105
|
+
def delete_by_password(encrypted: bytes) -> None:
|
|
106
|
+
"""
|
|
107
|
+
Deletes a password entry
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
db_path = str(get_db_path())
|
|
111
|
+
|
|
112
|
+
conn = sql.connect(db_path)
|
|
113
|
+
cur = conn.cursor()
|
|
114
|
+
|
|
115
|
+
cur.execute("DELETE FROM passwords WHERE password = ?", (encrypted,))
|
|
116
|
+
|
|
117
|
+
conn.commit()
|
|
118
|
+
conn.close()
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
from getpass import getpass
|
|
3
|
+
from pyperclip import copy
|
|
4
|
+
from os import urandom
|
|
5
|
+
from .db_handler import init_db, configure_vault, store_password, get_by_properties, get_all, delete_by_password
|
|
6
|
+
from .security import master_encrypt, verify_master_password, encrypt, decrypt, derive_key
|
|
7
|
+
from .utils import get_db_path, check_db_exists, get_vault_property, create_password
|
|
8
|
+
|
|
9
|
+
def init() -> None:
|
|
10
|
+
"""
|
|
11
|
+
Initialize the password manager's master password and the database
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
if check_db_exists() is True:
|
|
18
|
+
db_path = get_db_path()
|
|
19
|
+
console.print(f"[bold yellow]:| your database was already initialized at [italic]{db_path}")
|
|
20
|
+
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
console.print("[bold yellow]! initializing..\n")
|
|
24
|
+
|
|
25
|
+
master_password = getpass("> master password: ")
|
|
26
|
+
confirm = getpass("> enter the password again (confirm): ")
|
|
27
|
+
|
|
28
|
+
if master_password != confirm:
|
|
29
|
+
console.print("\n[bold red]:( passwords do not match, sorry -- aborting")
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
console.print("\n[bold green]:) matched!")
|
|
33
|
+
|
|
34
|
+
db_path = str(get_db_path())
|
|
35
|
+
init_db()
|
|
36
|
+
|
|
37
|
+
console.print(f"[bold green]:) database set up at [italic]{db_path}")
|
|
38
|
+
|
|
39
|
+
salt = urandom(16)
|
|
40
|
+
|
|
41
|
+
new_hash = master_encrypt(master_password)
|
|
42
|
+
configure_vault(salt, new_hash)
|
|
43
|
+
|
|
44
|
+
console.print("[bold green]:) master password successfully set up!\n")
|
|
45
|
+
console.print("[bold yellow]! do pitt -h for help")
|
|
46
|
+
except KeyboardInterrupt:
|
|
47
|
+
console.print("\n[bold red]bye!")
|
|
48
|
+
return
|
|
49
|
+
except Exception as e:
|
|
50
|
+
console.print(f"\n[bold red]:( error: {e}")
|
|
51
|
+
console.print("[bold yellow]! do pitt -h for help")
|
|
52
|
+
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
def add(service: str | None, username: str | None, note: str | None) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Adds a password to the password vault with the given service, username and a note
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
console = Console()
|
|
62
|
+
|
|
63
|
+
if service is None and username is None and note is None:
|
|
64
|
+
console.print("[bold red]:( you have to provide atleast a note, a username or a service")
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
master_password = getpass("> master password: ")
|
|
68
|
+
|
|
69
|
+
m_hash, salt = get_vault_property()
|
|
70
|
+
|
|
71
|
+
verify = verify_master_password(master_password, m_hash)
|
|
72
|
+
|
|
73
|
+
if verify is False:
|
|
74
|
+
console.print("[bold red]:( wrong password!")
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
password = create_password()
|
|
78
|
+
|
|
79
|
+
console.print("\n[bold green]:) password created!")
|
|
80
|
+
|
|
81
|
+
key = derive_key(master_password, salt)
|
|
82
|
+
encrypted = encrypt(key, password)
|
|
83
|
+
|
|
84
|
+
console.print("[bold green]:) password encrypted successfully!")
|
|
85
|
+
|
|
86
|
+
store_password(service, username, note, encrypted)
|
|
87
|
+
|
|
88
|
+
console.print("[bold green]:) password stored in database successfully!")
|
|
89
|
+
console.print("\n[bold yellow]! do pitt -h for help")
|
|
90
|
+
except KeyboardInterrupt:
|
|
91
|
+
console.print("\n[bold red]bye!")
|
|
92
|
+
return
|
|
93
|
+
except Exception as e:
|
|
94
|
+
console.print(f"\n[bold red]:( error: {e}")
|
|
95
|
+
console.print("[bold yellow]! do pitt -h for help")
|
|
96
|
+
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
def get(service: str | None, username: str | None) -> None:
|
|
100
|
+
"""
|
|
101
|
+
Copies a password to the clipboard
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
console = Console()
|
|
106
|
+
|
|
107
|
+
if service is None and username is None:
|
|
108
|
+
console.print("[bold red]:( you have to provide atleast a username or service")
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
master_password = getpass("> master password: ")
|
|
112
|
+
|
|
113
|
+
m_hash, salt = get_vault_property()
|
|
114
|
+
|
|
115
|
+
verify = verify_master_password(master_password, m_hash)
|
|
116
|
+
|
|
117
|
+
if verify is False:
|
|
118
|
+
console.print("[bold red]:( wrong password!")
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
console.print(f"[bold green]:) verification successful!")
|
|
122
|
+
|
|
123
|
+
results = get_by_properties(service, username)
|
|
124
|
+
|
|
125
|
+
if len(results) > 1:
|
|
126
|
+
console.print(f"\n[bold yellow]found passwords:\n")
|
|
127
|
+
|
|
128
|
+
for i, row in enumerate(results):
|
|
129
|
+
console.print(f"{i+1}. [bold]service: [/][honeydew2]{row[1]}[/], [bold]username: [/][light_cyan1]{row[2]}[/], [bold]note: [/][cornsilk1]{row[3]}")
|
|
130
|
+
|
|
131
|
+
console.print(f"\n[bold yellow]you have registered {len(results)} password that is saved with the given service or username")
|
|
132
|
+
|
|
133
|
+
p_index = int(console.input(f"[bold yellow]which one is the password to copy (index): "))
|
|
134
|
+
encrypted_password = results[p_index-1][4]
|
|
135
|
+
|
|
136
|
+
key = derive_key(master_password, salt)
|
|
137
|
+
byte_password = decrypt(key, encrypted_password)
|
|
138
|
+
|
|
139
|
+
password = byte_password.decode('utf-8')
|
|
140
|
+
|
|
141
|
+
copy(password)
|
|
142
|
+
|
|
143
|
+
console.print(f"\n[bold green]:) copied to the clipboard successfully!")
|
|
144
|
+
elif len(results) == 1:
|
|
145
|
+
row = results[0]
|
|
146
|
+
|
|
147
|
+
encrypted_password = row[4]
|
|
148
|
+
|
|
149
|
+
key = derive_key(master_password, salt)
|
|
150
|
+
byte_password = decrypt(key, encrypted_password)
|
|
151
|
+
|
|
152
|
+
password = byte_password.decode('utf-8')
|
|
153
|
+
|
|
154
|
+
copy(password)
|
|
155
|
+
|
|
156
|
+
console.print(f"[bold green]:) copied to the clipboard successfully!")
|
|
157
|
+
elif len(results) == 0:
|
|
158
|
+
console.print(f"\n[bold yellow]you have no registered password that is saved with the given service or username")
|
|
159
|
+
except KeyboardInterrupt:
|
|
160
|
+
console.print("\n[bold red]bye!")
|
|
161
|
+
return
|
|
162
|
+
except Exception as e:
|
|
163
|
+
console.print(f"\n[bold red]:( error: {e}")
|
|
164
|
+
console.print("[bold yellow]! do pitt -h for help")
|
|
165
|
+
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
def list_cmd() -> None:
|
|
169
|
+
"""
|
|
170
|
+
Lists all the saved passwords's details
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
console = Console()
|
|
175
|
+
|
|
176
|
+
master_password = getpass("> master password: ")
|
|
177
|
+
|
|
178
|
+
m_hash, _ = get_vault_property()
|
|
179
|
+
|
|
180
|
+
verify = verify_master_password(master_password, m_hash)
|
|
181
|
+
|
|
182
|
+
if verify is False:
|
|
183
|
+
console.print("[bold red]:( wrong password!")
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
console.print(f"[bold green]:) verification successful!")
|
|
187
|
+
|
|
188
|
+
passwords_results = get_all()
|
|
189
|
+
|
|
190
|
+
console.print(f"\n[bold yellow]saved passwords:")
|
|
191
|
+
|
|
192
|
+
for password in passwords_results:
|
|
193
|
+
console.print(f"{password[0]}. [bold]service: [/][honeydew2]{password[1]}[/], [bold]username: [/][light_cyan1]{password[2]}[/], [bold]note: [/][cornsilk1]{password[3]}")
|
|
194
|
+
except KeyboardInterrupt:
|
|
195
|
+
console.print("\n[bold red]bye!")
|
|
196
|
+
return
|
|
197
|
+
except Exception as e:
|
|
198
|
+
console.print(f"\n[bold red]:( error: {e}")
|
|
199
|
+
console.print("[bold yellow]! do pitt -h for help")
|
|
200
|
+
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
def del_cmd(service: str | None, username: str | None) -> None:
|
|
204
|
+
"""
|
|
205
|
+
Deletes a password from the password vault
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
console = Console()
|
|
210
|
+
|
|
211
|
+
if service is None and username is None:
|
|
212
|
+
console.print("[bold red]:( you have to provide atleast a username or service")
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
master_password = getpass("> master password: ")
|
|
216
|
+
|
|
217
|
+
m_hash, _ = get_vault_property()
|
|
218
|
+
|
|
219
|
+
verify = verify_master_password(master_password, m_hash)
|
|
220
|
+
|
|
221
|
+
if verify is False:
|
|
222
|
+
console.print("[bold red]:( wrong password!")
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
console.print(f"[bold green]:) verification successful!")
|
|
226
|
+
|
|
227
|
+
results = get_by_properties(service, username)
|
|
228
|
+
|
|
229
|
+
if len(results) > 1:
|
|
230
|
+
console.print(f"\n[bold yellow]found passwords:\n")
|
|
231
|
+
|
|
232
|
+
for i, row in enumerate(results):
|
|
233
|
+
console.print(f"{i+1}. [bold]service: [/][honeydew2]{row[1]}[/], [bold]username: [/][light_cyan1]{row[2]}[/], [bold]note: [/][cornsilk1]{row[3]}")
|
|
234
|
+
|
|
235
|
+
console.print(f"\n[bold yellow]you have registered {len(results)} password that is saved with the given service or username")
|
|
236
|
+
|
|
237
|
+
p_index = int(console.input(f"[bold yellow]which one is the password to delete (index): "))
|
|
238
|
+
encrypted_password = results[p_index-1][4]
|
|
239
|
+
|
|
240
|
+
delete_by_password(encrypted_password)
|
|
241
|
+
elif len(results) == 1:
|
|
242
|
+
encrypted_password = results[0][4]
|
|
243
|
+
|
|
244
|
+
res = None
|
|
245
|
+
console.print()
|
|
246
|
+
|
|
247
|
+
while res not in ('y', 'n', ''):
|
|
248
|
+
res = console.input("[bold yellow]! are you sure about this (y/N): ").lower()
|
|
249
|
+
|
|
250
|
+
if res == 'y':
|
|
251
|
+
res2 = None
|
|
252
|
+
console.print()
|
|
253
|
+
|
|
254
|
+
while res2 not in ('y', 'n', ''):
|
|
255
|
+
res2 = console.input("[bold yellow]! are you really sure about this (y/N): ").lower()
|
|
256
|
+
|
|
257
|
+
if res2 == '' or res == 'n':
|
|
258
|
+
return
|
|
259
|
+
elif res == '' or res == 'n':
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
delete_by_password(encrypted_password)
|
|
263
|
+
elif len(results) == 0:
|
|
264
|
+
console.print(f"\n[bold yellow]you have no registered password that is saved with the given service or username")
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
console.print(f"\n[bold green]:) deletion successful!")
|
|
268
|
+
except KeyboardInterrupt:
|
|
269
|
+
console.print("\n[bold red]bye!")
|
|
270
|
+
return
|
|
271
|
+
except Exception as e:
|
|
272
|
+
console.print(f"\n[bold red]:( error: {e}")
|
|
273
|
+
console.print("[bold yellow]! do pitt -h for help")
|
|
274
|
+
|
|
275
|
+
return
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .functions import init, add, get, list_cmd, del_cmd
|
|
2
|
+
from .parse import parse
|
|
3
|
+
|
|
4
|
+
def main() -> None:
|
|
5
|
+
args = parse()
|
|
6
|
+
|
|
7
|
+
if args.command == "init":
|
|
8
|
+
init()
|
|
9
|
+
elif args.command == "add":
|
|
10
|
+
add(args.service, args.username, args.note)
|
|
11
|
+
elif args.command == "get":
|
|
12
|
+
get(args.service, args.username)
|
|
13
|
+
elif args.command == "list":
|
|
14
|
+
list_cmd()
|
|
15
|
+
elif args.command == "del":
|
|
16
|
+
del_cmd(args.service, args.username)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
def parse() -> argparse.Namespace:
|
|
4
|
+
"""
|
|
5
|
+
Returns the parsed arguments
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
parser = argparse.ArgumentParser(allow_abbrev=False, prog="pitt")
|
|
9
|
+
subparser = parser.add_subparsers(dest="command", required=True, help="commands")
|
|
10
|
+
|
|
11
|
+
subparser.add_parser("init", help="set up a master password to initialize the password pit of doom and despair")
|
|
12
|
+
subparser.add_parser("list", help="lists all the saved password")
|
|
13
|
+
|
|
14
|
+
del_password_parser = subparser.add_parser("del", help="delete a saved password")
|
|
15
|
+
get_password_parser = subparser.add_parser("get", help="copy a stored password to the clipboard")
|
|
16
|
+
add_password_parser = subparser.add_parser("add", help="generate and store a new randomized password")
|
|
17
|
+
|
|
18
|
+
del_password_parser.add_argument("-s", "--service", help="specify the service assigned to the password that is to be deleted")
|
|
19
|
+
del_password_parser.add_argument("-u", "--username", help="specify the username assigned to the password that is to be copied to clipboard")
|
|
20
|
+
|
|
21
|
+
get_password_parser.add_argument("-s", "--service", help="specify the service assigned to the password that is to be copied to clipboard")
|
|
22
|
+
get_password_parser.add_argument("-u", "--username", help="specify the username assigned to the password that is to be copied to clipboard")
|
|
23
|
+
|
|
24
|
+
add_password_parser.add_argument("-s", "--service", help="specify the service that the new generated password belongs to")
|
|
25
|
+
add_password_parser.add_argument("-u", "--username", help="specify the username that the new generated password belongs to")
|
|
26
|
+
add_password_parser.add_argument("-n", "--note", help="specify notes about this particular password")
|
|
27
|
+
|
|
28
|
+
args = parser.parse_args()
|
|
29
|
+
|
|
30
|
+
return args
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from argon2 import PasswordHasher, exceptions, low_level, Type
|
|
2
|
+
from cryptography.fernet import Fernet
|
|
3
|
+
import base64
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
def master_encrypt(password: str) -> str:
|
|
7
|
+
"""
|
|
8
|
+
Returns the encrypted hash of a given master password
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
kdf = PasswordHasher()
|
|
12
|
+
hash = kdf.hash(password)
|
|
13
|
+
|
|
14
|
+
return hash
|
|
15
|
+
|
|
16
|
+
def verify_master_password(password: str, hash: str) -> bool:
|
|
17
|
+
"""
|
|
18
|
+
Verifies whether the inputted master password matches with the stored master password
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
kdf = PasswordHasher()
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
kdf.verify(hash, password)
|
|
25
|
+
except exceptions.VerifyMismatchError:
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
def derive_key(password: str, salt: bytes) -> bytes:
|
|
31
|
+
"""
|
|
32
|
+
Derives the 64-byte Argon2id key from the password
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
key = low_level.hash_secret_raw(password.encode("utf-8"), salt, 3, 65536, 1, 32, Type.ID)
|
|
36
|
+
|
|
37
|
+
b64_key = base64.urlsafe_b64encode(key)
|
|
38
|
+
|
|
39
|
+
return b64_key
|
|
40
|
+
|
|
41
|
+
def encrypt(key: bytes, password: str) -> bytes:
|
|
42
|
+
"""
|
|
43
|
+
Encrypts the given password using Fernet
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
f = Fernet(key)
|
|
47
|
+
token = f.encrypt(password.encode("utf-8"))
|
|
48
|
+
|
|
49
|
+
return token
|
|
50
|
+
|
|
51
|
+
def decrypt(key: bytes, encrypted: bytes) -> bytes:
|
|
52
|
+
"""
|
|
53
|
+
Decrypts the given encrypted password using Fernet
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
f = Fernet(key)
|
|
57
|
+
password = f.decrypt(encrypted)
|
|
58
|
+
|
|
59
|
+
return password
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import sqlite3 as sql
|
|
2
|
+
from random import choices, randint
|
|
3
|
+
from platformdirs import PlatformDirs
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
def get_db_path() -> Path:
|
|
7
|
+
"""
|
|
8
|
+
Returns the default database path for all operating systems
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
dirs = PlatformDirs("pitt")
|
|
12
|
+
return dirs.user_data_path / "passwords.db"
|
|
13
|
+
|
|
14
|
+
def check_db_exists() -> bool:
|
|
15
|
+
"""
|
|
16
|
+
Returns whether the password vault database exists
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
db_path = get_db_path()
|
|
20
|
+
return db_path.is_file()
|
|
21
|
+
|
|
22
|
+
def check_master_password_exists() -> bool:
|
|
23
|
+
"""
|
|
24
|
+
Returns whether the master password has been set up in the password vault database
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
if check_db_exists() is False:
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
db_path = get_db_path()
|
|
31
|
+
|
|
32
|
+
conn = sql.connect(db_path)
|
|
33
|
+
cur = conn.cursor()
|
|
34
|
+
|
|
35
|
+
cur.execute("SELECT master_hash FROM master")
|
|
36
|
+
|
|
37
|
+
vals = cur.fetchone()
|
|
38
|
+
conn.close()
|
|
39
|
+
|
|
40
|
+
if len(vals) == 0:
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
return True
|
|
44
|
+
|
|
45
|
+
def get_vault_property() -> tuple:
|
|
46
|
+
"""
|
|
47
|
+
Returns master vault's properties
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
if check_db_exists() is False:
|
|
51
|
+
raise RuntimeError("database doesn't exist.")
|
|
52
|
+
elif check_master_password_exists() is False:
|
|
53
|
+
raise RuntimeError("master password isn't set yet the database file is created.")
|
|
54
|
+
|
|
55
|
+
db_path = get_db_path()
|
|
56
|
+
|
|
57
|
+
conn = sql.connect(db_path)
|
|
58
|
+
cur = conn.cursor()
|
|
59
|
+
|
|
60
|
+
cur.execute("SELECT master_hash, salt FROM master")
|
|
61
|
+
|
|
62
|
+
rows = cur.fetchone()
|
|
63
|
+
conn.close()
|
|
64
|
+
|
|
65
|
+
return rows
|
|
66
|
+
|
|
67
|
+
def create_password() -> str:
|
|
68
|
+
"""
|
|
69
|
+
Creates a random password
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
items = ['"', '`', '1', '2', '3','4','5','6','7','8','9','0','-','=','q','w','e','r','t','y','u','i','o','p','[',']','\\','a','s','d','f','g','h','j','k','l',';','z','x','c','v','b','n','m',',','.','/','~','!','@','#',"$","%","^","&","*",'(',')','_','+','Q','W','E','R','T','Y','U','I','O','P','{','}','|','A','S','D','F','G','H','J','K','L',':','Z','X','C','V','B','N','M','<','>','?']
|
|
73
|
+
|
|
74
|
+
p_len = randint(35,50)
|
|
75
|
+
list_char = choices(items, k=p_len)
|
|
76
|
+
|
|
77
|
+
password = ''.join(list_char)
|
|
78
|
+
|
|
79
|
+
return password
|