revault-cli 1.0.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.
- revault_cli-1.0.0/PKG-INFO +111 -0
- revault_cli-1.0.0/Readme.md +98 -0
- revault_cli-1.0.0/cli.py +172 -0
- revault_cli-1.0.0/crypto.py +26 -0
- revault_cli-1.0.0/pyproject.toml +26 -0
- revault_cli-1.0.0/revault_cli.egg-info/PKG-INFO +111 -0
- revault_cli-1.0.0/revault_cli.egg-info/SOURCES.txt +11 -0
- revault_cli-1.0.0/revault_cli.egg-info/dependency_links.txt +1 -0
- revault_cli-1.0.0/revault_cli.egg-info/entry_points.txt +2 -0
- revault_cli-1.0.0/revault_cli.egg-info/requires.txt +6 -0
- revault_cli-1.0.0/revault_cli.egg-info/top_level.txt +3 -0
- revault_cli-1.0.0/setup.cfg +4 -0
- revault_cli-1.0.0/vault.py +77 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: revault-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: CLI password manager for server credentials with SSH connector
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/sufi-an/re-vault
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: typer>=0.9.0
|
|
10
|
+
Requires-Dist: cryptography>=41.0.3
|
|
11
|
+
Requires-Dist: pexpect>=4.8.0; sys_platform != "win32"
|
|
12
|
+
Requires-Dist: paramiko>=3.4.0
|
|
13
|
+
|
|
14
|
+
Author was lost in a sea of server credentials, brain.exe could not handle so much info
|
|
15
|
+
Wrote a password manager to store server credentails(Yes, ENCRYPTED)
|
|
16
|
+
then thought why should I connect.
|
|
17
|
+
so
|
|
18
|
+
re-valut connect myserver :)
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
### macOS (Homebrew)
|
|
23
|
+
```bash
|
|
24
|
+
brew tap sufi-an/re-vault
|
|
25
|
+
brew install re-vault
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Linux / macOS (pip / pipx)
|
|
29
|
+
```bash
|
|
30
|
+
# pipx keeps it isolated (recommended)
|
|
31
|
+
pipx install revault-cli
|
|
32
|
+
|
|
33
|
+
# or plain pip
|
|
34
|
+
pip install revault-cli
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then use `re-vault` instead of `python cli.py`:
|
|
38
|
+
```bash
|
|
39
|
+
re-vault add myserver
|
|
40
|
+
re-vault connect myserver
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
> **apt install:** Not yet in official Debian/Ubuntu repos (that process takes months).
|
|
44
|
+
> Use `pip`/`pipx` on Linux for now.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Windows .exe
|
|
49
|
+
|
|
50
|
+
**Quick test (run on your Windows machine):**
|
|
51
|
+
```bat
|
|
52
|
+
build.bat
|
|
53
|
+
dist\passkeeper.exe add myserver
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Automated releases:** push a version tag and GitHub Actions builds + attaches the exe to the release automatically.
|
|
57
|
+
```bash
|
|
58
|
+
git tag v1.0.0 && git push origin v1.0.0
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## How to use
|
|
64
|
+
|
|
65
|
+
### Install
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install -r requirements.txt
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Commands
|
|
72
|
+
|
|
73
|
+
Each command prompts for your **master password** (it is never stored). The vault
|
|
74
|
+
lives at `~/.passkeeper/vault.json` and is locked to your user account on both
|
|
75
|
+
Unix (`chmod 0o600`) and Windows (`icacls`).
|
|
76
|
+
|
|
77
|
+
| Command | What it does |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `python cli.py add <identifier>` | Prompt for username + password and store them encrypted under `<identifier>`. |
|
|
80
|
+
| `python cli.py show <identifier>` | Decrypt and print the username + password. |
|
|
81
|
+
| `python cli.py list` | List all stored identifiers. |
|
|
82
|
+
| `python cli.py connect <identifier>` | Decrypt the entry and SSH in, auto-sending the password (works on Unix **and** Windows). |
|
|
83
|
+
| `python cli.py remove <identifier>` | Delete a stored entry permanently. |
|
|
84
|
+
|
|
85
|
+
### Example
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Store credentials for a server (username should be the full SSH target, e.g. user@host)
|
|
89
|
+
$ python cli.py add myserver
|
|
90
|
+
Master password:
|
|
91
|
+
Username: deploy@198.51.100.10
|
|
92
|
+
Password:
|
|
93
|
+
Entry 'myserver' added successfully!
|
|
94
|
+
|
|
95
|
+
# Look them up later
|
|
96
|
+
$ python cli.py show myserver
|
|
97
|
+
Master password:
|
|
98
|
+
Username: deploy@198.51.100.10
|
|
99
|
+
Password: ...
|
|
100
|
+
|
|
101
|
+
# List everything you've stored
|
|
102
|
+
$ python cli.py list
|
|
103
|
+
Stored entries:
|
|
104
|
+
- myserver
|
|
105
|
+
|
|
106
|
+
# Connect over SSH without typing the password (Unix/macOS/Windows)
|
|
107
|
+
$ python cli.py connect myserver
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
> **Note:** On Unix/macOS `connect` uses `pexpect` to drive the system `ssh` binary.
|
|
111
|
+
> On Windows it uses `paramiko` to connect directly — no external `ssh` binary needed.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Author was lost in a sea of server credentials, brain.exe could not handle so much info
|
|
2
|
+
Wrote a password manager to store server credentails(Yes, ENCRYPTED)
|
|
3
|
+
then thought why should I connect.
|
|
4
|
+
so
|
|
5
|
+
re-valut connect myserver :)
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
### macOS (Homebrew)
|
|
10
|
+
```bash
|
|
11
|
+
brew tap sufi-an/re-vault
|
|
12
|
+
brew install re-vault
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Linux / macOS (pip / pipx)
|
|
16
|
+
```bash
|
|
17
|
+
# pipx keeps it isolated (recommended)
|
|
18
|
+
pipx install revault-cli
|
|
19
|
+
|
|
20
|
+
# or plain pip
|
|
21
|
+
pip install revault-cli
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Then use `re-vault` instead of `python cli.py`:
|
|
25
|
+
```bash
|
|
26
|
+
re-vault add myserver
|
|
27
|
+
re-vault connect myserver
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
> **apt install:** Not yet in official Debian/Ubuntu repos (that process takes months).
|
|
31
|
+
> Use `pip`/`pipx` on Linux for now.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Windows .exe
|
|
36
|
+
|
|
37
|
+
**Quick test (run on your Windows machine):**
|
|
38
|
+
```bat
|
|
39
|
+
build.bat
|
|
40
|
+
dist\passkeeper.exe add myserver
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Automated releases:** push a version tag and GitHub Actions builds + attaches the exe to the release automatically.
|
|
44
|
+
```bash
|
|
45
|
+
git tag v1.0.0 && git push origin v1.0.0
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## How to use
|
|
51
|
+
|
|
52
|
+
### Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install -r requirements.txt
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Commands
|
|
59
|
+
|
|
60
|
+
Each command prompts for your **master password** (it is never stored). The vault
|
|
61
|
+
lives at `~/.passkeeper/vault.json` and is locked to your user account on both
|
|
62
|
+
Unix (`chmod 0o600`) and Windows (`icacls`).
|
|
63
|
+
|
|
64
|
+
| Command | What it does |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| `python cli.py add <identifier>` | Prompt for username + password and store them encrypted under `<identifier>`. |
|
|
67
|
+
| `python cli.py show <identifier>` | Decrypt and print the username + password. |
|
|
68
|
+
| `python cli.py list` | List all stored identifiers. |
|
|
69
|
+
| `python cli.py connect <identifier>` | Decrypt the entry and SSH in, auto-sending the password (works on Unix **and** Windows). |
|
|
70
|
+
| `python cli.py remove <identifier>` | Delete a stored entry permanently. |
|
|
71
|
+
|
|
72
|
+
### Example
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Store credentials for a server (username should be the full SSH target, e.g. user@host)
|
|
76
|
+
$ python cli.py add myserver
|
|
77
|
+
Master password:
|
|
78
|
+
Username: deploy@198.51.100.10
|
|
79
|
+
Password:
|
|
80
|
+
Entry 'myserver' added successfully!
|
|
81
|
+
|
|
82
|
+
# Look them up later
|
|
83
|
+
$ python cli.py show myserver
|
|
84
|
+
Master password:
|
|
85
|
+
Username: deploy@198.51.100.10
|
|
86
|
+
Password: ...
|
|
87
|
+
|
|
88
|
+
# List everything you've stored
|
|
89
|
+
$ python cli.py list
|
|
90
|
+
Stored entries:
|
|
91
|
+
- myserver
|
|
92
|
+
|
|
93
|
+
# Connect over SSH without typing the password (Unix/macOS/Windows)
|
|
94
|
+
$ python cli.py connect myserver
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
> **Note:** On Unix/macOS `connect` uses `pexpect` to drive the system `ssh` binary.
|
|
98
|
+
> On Windows it uses `paramiko` to connect directly — no external `ssh` binary needed.
|
revault_cli-1.0.0/cli.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# cli.py
|
|
2
|
+
import typer
|
|
3
|
+
from getpass import getpass
|
|
4
|
+
import json
|
|
5
|
+
import base64
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
import threading
|
|
9
|
+
from vault import init_vault, add_entry, get_entry, remove_entry, list_entries, get_salt
|
|
10
|
+
from crypto import derive_key, encrypt_data, decrypt_data
|
|
11
|
+
|
|
12
|
+
app = typer.Typer()
|
|
13
|
+
|
|
14
|
+
@app.command()
|
|
15
|
+
def add(identifier: str):
|
|
16
|
+
"""Add username + password for a service."""
|
|
17
|
+
vault = init_vault()
|
|
18
|
+
master_password = getpass("Master password: ")
|
|
19
|
+
key = derive_key(master_password, get_salt(vault))
|
|
20
|
+
|
|
21
|
+
username = input("Username: ")
|
|
22
|
+
password = getpass("Password: ")
|
|
23
|
+
|
|
24
|
+
# Prepare entry JSON
|
|
25
|
+
entry_json = json.dumps({"username": username, "password": password}).encode()
|
|
26
|
+
encrypted_blob = encrypt_data(key, entry_json)
|
|
27
|
+
encrypted_b64 = base64.b64encode(encrypted_blob).decode()
|
|
28
|
+
|
|
29
|
+
add_entry(vault, identifier, encrypted_b64)
|
|
30
|
+
typer.echo(f"Entry '{identifier}' added successfully!")
|
|
31
|
+
|
|
32
|
+
@app.command()
|
|
33
|
+
def show(identifier: str):
|
|
34
|
+
"""Show username + password for a service."""
|
|
35
|
+
vault = init_vault()
|
|
36
|
+
encrypted_b64 = get_entry(vault, identifier)
|
|
37
|
+
if not encrypted_b64:
|
|
38
|
+
typer.echo(f"No entry found for '{identifier}'")
|
|
39
|
+
raise typer.Exit()
|
|
40
|
+
|
|
41
|
+
master_password = getpass("Master password: ")
|
|
42
|
+
key = derive_key(master_password, get_salt(vault))
|
|
43
|
+
|
|
44
|
+
encrypted_blob = base64.b64decode(encrypted_b64)
|
|
45
|
+
try:
|
|
46
|
+
decrypted_bytes = decrypt_data(key, encrypted_blob)
|
|
47
|
+
entry = json.loads(decrypted_bytes.decode())
|
|
48
|
+
typer.echo(f"Username: {entry['username']}")
|
|
49
|
+
typer.echo(f"Password: {entry['password']}")
|
|
50
|
+
except Exception:
|
|
51
|
+
typer.echo("Incorrect master password or corrupted entry!")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _connect_pexpect(ssh_target: str, password: str):
|
|
55
|
+
import pexpect
|
|
56
|
+
child = pexpect.spawn(f"ssh {ssh_target}")
|
|
57
|
+
child.expect("password:")
|
|
58
|
+
child.sendline(password)
|
|
59
|
+
child.interact()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _connect_paramiko(ssh_target: str, password: str):
|
|
63
|
+
import paramiko
|
|
64
|
+
|
|
65
|
+
if "@" not in ssh_target:
|
|
66
|
+
typer.echo("Error: SSH target must be in 'user@host' format.")
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
user, host = ssh_target.split("@", 1)
|
|
70
|
+
client = paramiko.SSHClient()
|
|
71
|
+
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
client.connect(host, username=user, password=password)
|
|
75
|
+
except paramiko.AuthenticationException:
|
|
76
|
+
typer.echo("Authentication failed: wrong password.")
|
|
77
|
+
return
|
|
78
|
+
except Exception as e:
|
|
79
|
+
typer.echo(f"Connection failed: {e}")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
channel = client.invoke_shell()
|
|
83
|
+
stop = threading.Event()
|
|
84
|
+
|
|
85
|
+
def _read_output():
|
|
86
|
+
while not stop.is_set():
|
|
87
|
+
try:
|
|
88
|
+
data = channel.recv(4096)
|
|
89
|
+
if not data:
|
|
90
|
+
stop.set()
|
|
91
|
+
break
|
|
92
|
+
sys.stdout.buffer.write(data)
|
|
93
|
+
sys.stdout.buffer.flush()
|
|
94
|
+
except Exception:
|
|
95
|
+
stop.set()
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
reader = threading.Thread(target=_read_output, daemon=True)
|
|
99
|
+
reader.start()
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
import msvcrt
|
|
103
|
+
while not stop.is_set():
|
|
104
|
+
if msvcrt.kbhit():
|
|
105
|
+
ch = msvcrt.getch()
|
|
106
|
+
if not channel.send(ch):
|
|
107
|
+
break
|
|
108
|
+
else:
|
|
109
|
+
time.sleep(0.05)
|
|
110
|
+
except KeyboardInterrupt:
|
|
111
|
+
pass
|
|
112
|
+
finally:
|
|
113
|
+
stop.set()
|
|
114
|
+
channel.close()
|
|
115
|
+
client.close()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.command()
|
|
119
|
+
def connect(identifier: str):
|
|
120
|
+
"""SSH into a host using stored credentials."""
|
|
121
|
+
vault = init_vault()
|
|
122
|
+
encrypted_b64 = get_entry(vault, identifier)
|
|
123
|
+
if not encrypted_b64:
|
|
124
|
+
typer.echo(f"No entry found for '{identifier}'")
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
master_password = getpass("Master password: ")
|
|
128
|
+
key = derive_key(master_password, get_salt(vault))
|
|
129
|
+
encrypted_blob = base64.b64decode(encrypted_b64)
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
decrypted_bytes = decrypt_data(key, encrypted_blob)
|
|
133
|
+
entry = json.loads(decrypted_bytes.decode())
|
|
134
|
+
except Exception:
|
|
135
|
+
typer.echo("Incorrect master password or corrupted entry!")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
ssh_target = entry["username"]
|
|
139
|
+
password = entry["password"]
|
|
140
|
+
|
|
141
|
+
typer.echo(f"Connecting to {ssh_target}...")
|
|
142
|
+
|
|
143
|
+
if sys.platform == "win32":
|
|
144
|
+
_connect_paramiko(ssh_target, password)
|
|
145
|
+
else:
|
|
146
|
+
_connect_pexpect(ssh_target, password)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@app.command()
|
|
150
|
+
def remove(identifier: str):
|
|
151
|
+
"""Remove a stored entry."""
|
|
152
|
+
vault = init_vault()
|
|
153
|
+
if not remove_entry(vault, identifier):
|
|
154
|
+
typer.echo(f"No entry found for '{identifier}'")
|
|
155
|
+
raise typer.Exit(1)
|
|
156
|
+
typer.echo(f"Entry '{identifier}' removed.")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@app.command()
|
|
160
|
+
def list():
|
|
161
|
+
"""List all stored identifiers."""
|
|
162
|
+
vault = init_vault()
|
|
163
|
+
entries = list_entries(vault)
|
|
164
|
+
if entries:
|
|
165
|
+
typer.echo("Stored entries:")
|
|
166
|
+
for e in entries:
|
|
167
|
+
typer.echo(f"- {e}")
|
|
168
|
+
else:
|
|
169
|
+
typer.echo("No entries stored yet.")
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
app()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# crypto.py
|
|
2
|
+
import base64
|
|
3
|
+
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
|
|
4
|
+
from cryptography.fernet import Fernet
|
|
5
|
+
|
|
6
|
+
def derive_key(master_password: str, salt: bytes) -> bytes:
|
|
7
|
+
"""
|
|
8
|
+
Derive a symmetric key from master password and salt
|
|
9
|
+
"""
|
|
10
|
+
kdf = Scrypt(
|
|
11
|
+
salt=salt,
|
|
12
|
+
length=32,
|
|
13
|
+
n=2**14,
|
|
14
|
+
r=8,
|
|
15
|
+
p=1,
|
|
16
|
+
)
|
|
17
|
+
key = kdf.derive(master_password.encode())
|
|
18
|
+
return base64.urlsafe_b64encode(key) # Fernet requires base64 key
|
|
19
|
+
|
|
20
|
+
def encrypt_data(key: bytes, data: bytes) -> bytes:
|
|
21
|
+
f = Fernet(key)
|
|
22
|
+
return f.encrypt(data)
|
|
23
|
+
|
|
24
|
+
def decrypt_data(key: bytes, token: bytes) -> bytes:
|
|
25
|
+
f = Fernet(key)
|
|
26
|
+
return f.decrypt(token)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "revault-cli"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "CLI password manager for server credentials with SSH connector"
|
|
9
|
+
readme = "Readme.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
dependencies = [
|
|
13
|
+
"typer>=0.9.0",
|
|
14
|
+
"cryptography>=41.0.3",
|
|
15
|
+
"pexpect>=4.8.0; sys_platform != 'win32'",
|
|
16
|
+
"paramiko>=3.4.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://github.com/sufi-an/re-vault"
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
re-vault = "cli:app"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools]
|
|
26
|
+
py-modules = ["cli", "vault", "crypto"]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: revault-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: CLI password manager for server credentials with SSH connector
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/sufi-an/re-vault
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: typer>=0.9.0
|
|
10
|
+
Requires-Dist: cryptography>=41.0.3
|
|
11
|
+
Requires-Dist: pexpect>=4.8.0; sys_platform != "win32"
|
|
12
|
+
Requires-Dist: paramiko>=3.4.0
|
|
13
|
+
|
|
14
|
+
Author was lost in a sea of server credentials, brain.exe could not handle so much info
|
|
15
|
+
Wrote a password manager to store server credentails(Yes, ENCRYPTED)
|
|
16
|
+
then thought why should I connect.
|
|
17
|
+
so
|
|
18
|
+
re-valut connect myserver :)
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
### macOS (Homebrew)
|
|
23
|
+
```bash
|
|
24
|
+
brew tap sufi-an/re-vault
|
|
25
|
+
brew install re-vault
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Linux / macOS (pip / pipx)
|
|
29
|
+
```bash
|
|
30
|
+
# pipx keeps it isolated (recommended)
|
|
31
|
+
pipx install revault-cli
|
|
32
|
+
|
|
33
|
+
# or plain pip
|
|
34
|
+
pip install revault-cli
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then use `re-vault` instead of `python cli.py`:
|
|
38
|
+
```bash
|
|
39
|
+
re-vault add myserver
|
|
40
|
+
re-vault connect myserver
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
> **apt install:** Not yet in official Debian/Ubuntu repos (that process takes months).
|
|
44
|
+
> Use `pip`/`pipx` on Linux for now.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Windows .exe
|
|
49
|
+
|
|
50
|
+
**Quick test (run on your Windows machine):**
|
|
51
|
+
```bat
|
|
52
|
+
build.bat
|
|
53
|
+
dist\passkeeper.exe add myserver
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Automated releases:** push a version tag and GitHub Actions builds + attaches the exe to the release automatically.
|
|
57
|
+
```bash
|
|
58
|
+
git tag v1.0.0 && git push origin v1.0.0
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## How to use
|
|
64
|
+
|
|
65
|
+
### Install
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install -r requirements.txt
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Commands
|
|
72
|
+
|
|
73
|
+
Each command prompts for your **master password** (it is never stored). The vault
|
|
74
|
+
lives at `~/.passkeeper/vault.json` and is locked to your user account on both
|
|
75
|
+
Unix (`chmod 0o600`) and Windows (`icacls`).
|
|
76
|
+
|
|
77
|
+
| Command | What it does |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `python cli.py add <identifier>` | Prompt for username + password and store them encrypted under `<identifier>`. |
|
|
80
|
+
| `python cli.py show <identifier>` | Decrypt and print the username + password. |
|
|
81
|
+
| `python cli.py list` | List all stored identifiers. |
|
|
82
|
+
| `python cli.py connect <identifier>` | Decrypt the entry and SSH in, auto-sending the password (works on Unix **and** Windows). |
|
|
83
|
+
| `python cli.py remove <identifier>` | Delete a stored entry permanently. |
|
|
84
|
+
|
|
85
|
+
### Example
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Store credentials for a server (username should be the full SSH target, e.g. user@host)
|
|
89
|
+
$ python cli.py add myserver
|
|
90
|
+
Master password:
|
|
91
|
+
Username: deploy@198.51.100.10
|
|
92
|
+
Password:
|
|
93
|
+
Entry 'myserver' added successfully!
|
|
94
|
+
|
|
95
|
+
# Look them up later
|
|
96
|
+
$ python cli.py show myserver
|
|
97
|
+
Master password:
|
|
98
|
+
Username: deploy@198.51.100.10
|
|
99
|
+
Password: ...
|
|
100
|
+
|
|
101
|
+
# List everything you've stored
|
|
102
|
+
$ python cli.py list
|
|
103
|
+
Stored entries:
|
|
104
|
+
- myserver
|
|
105
|
+
|
|
106
|
+
# Connect over SSH without typing the password (Unix/macOS/Windows)
|
|
107
|
+
$ python cli.py connect myserver
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
> **Note:** On Unix/macOS `connect` uses `pexpect` to drive the system `ssh` binary.
|
|
111
|
+
> On Windows it uses `paramiko` to connect directly — no external `ssh` binary needed.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Readme.md
|
|
2
|
+
cli.py
|
|
3
|
+
crypto.py
|
|
4
|
+
pyproject.toml
|
|
5
|
+
vault.py
|
|
6
|
+
revault_cli.egg-info/PKG-INFO
|
|
7
|
+
revault_cli.egg-info/SOURCES.txt
|
|
8
|
+
revault_cli.egg-info/dependency_links.txt
|
|
9
|
+
revault_cli.egg-info/entry_points.txt
|
|
10
|
+
revault_cli.egg-info/requires.txt
|
|
11
|
+
revault_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# vault.py
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import os
|
|
5
|
+
import base64
|
|
6
|
+
import getpass
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
VAULT_DIR = Path.home() / ".passkeeper"
|
|
10
|
+
VAULT_FILE = VAULT_DIR / "vault.json"
|
|
11
|
+
|
|
12
|
+
def _restrict_to_current_user(path: Path):
|
|
13
|
+
"""Restrict a path so only the current user can read/write it.
|
|
14
|
+
|
|
15
|
+
On POSIX this is a simple chmod 0o600/0o700. On Windows os.chmod can only
|
|
16
|
+
toggle the read-only bit and cannot express per-user access, so the secrets
|
|
17
|
+
would stay readable by every account on the machine. We use icacls instead
|
|
18
|
+
to drop inherited permissions and grant access to the current user only.
|
|
19
|
+
"""
|
|
20
|
+
if os.name == "nt":
|
|
21
|
+
user = os.environ.get("USERNAME") or getpass.getuser()
|
|
22
|
+
try:
|
|
23
|
+
subprocess.run(
|
|
24
|
+
["icacls", str(path), "/inheritance:r", "/grant:r", f"{user}:(F)"],
|
|
25
|
+
check=True,
|
|
26
|
+
capture_output=True,
|
|
27
|
+
)
|
|
28
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
29
|
+
# icacls unavailable (e.g. some minimal environments) — fall back to
|
|
30
|
+
# the read-only bit so we at least do something.
|
|
31
|
+
os.chmod(path, 0o600)
|
|
32
|
+
else:
|
|
33
|
+
mode = 0o700 if path.is_dir() else 0o600
|
|
34
|
+
os.chmod(path, mode)
|
|
35
|
+
|
|
36
|
+
def init_vault():
|
|
37
|
+
VAULT_DIR.mkdir(exist_ok=True)
|
|
38
|
+
_restrict_to_current_user(VAULT_DIR)
|
|
39
|
+
if not VAULT_FILE.exists():
|
|
40
|
+
# Generate random salt
|
|
41
|
+
salt = os.urandom(16)
|
|
42
|
+
vault = {
|
|
43
|
+
"version": 1,
|
|
44
|
+
"salt": base64.b64encode(salt).decode(),
|
|
45
|
+
"entries": {}
|
|
46
|
+
}
|
|
47
|
+
save_vault(vault)
|
|
48
|
+
_restrict_to_current_user(VAULT_FILE)
|
|
49
|
+
return load_vault()
|
|
50
|
+
|
|
51
|
+
def load_vault():
|
|
52
|
+
with VAULT_FILE.open("r", encoding="utf-8") as f:
|
|
53
|
+
return json.load(f)
|
|
54
|
+
|
|
55
|
+
def save_vault(vault: dict):
|
|
56
|
+
with VAULT_FILE.open("w", encoding="utf-8") as f:
|
|
57
|
+
json.dump(vault, f, indent=4)
|
|
58
|
+
|
|
59
|
+
def get_salt(vault: dict) -> bytes:
|
|
60
|
+
return base64.b64decode(vault["salt"])
|
|
61
|
+
|
|
62
|
+
def add_entry(vault: dict, identifier: str, encrypted_blob: str):
|
|
63
|
+
vault["entries"][identifier] = encrypted_blob
|
|
64
|
+
save_vault(vault)
|
|
65
|
+
|
|
66
|
+
def get_entry(vault: dict, identifier: str):
|
|
67
|
+
return vault["entries"].get(identifier)
|
|
68
|
+
|
|
69
|
+
def remove_entry(vault: dict, identifier: str) -> bool:
|
|
70
|
+
if identifier not in vault["entries"]:
|
|
71
|
+
return False
|
|
72
|
+
del vault["entries"][identifier]
|
|
73
|
+
save_vault(vault)
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
def list_entries(vault: dict):
|
|
77
|
+
return list(vault["entries"].keys())
|