jkey 0.1.2__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.
- jkey-0.1.2/PKG-INFO +105 -0
- jkey-0.1.2/README.md +93 -0
- jkey-0.1.2/pyproject.toml +36 -0
- jkey-0.1.2/setup.cfg +4 -0
- jkey-0.1.2/src/jkey/2fa/__init__.py +0 -0
- jkey-0.1.2/src/jkey/2fa/core.py +124 -0
- jkey-0.1.2/src/jkey/2fa/qr.py +65 -0
- jkey-0.1.2/src/jkey/__init__.py +0 -0
- jkey-0.1.2/src/jkey/__main__.py +4 -0
- jkey-0.1.2/src/jkey/aes.py +279 -0
- jkey-0.1.2/src/jkey/cli.py +154 -0
- jkey-0.1.2/src/jkey/pm/__init__.py +0 -0
- jkey-0.1.2/src/jkey/pm/core.py +86 -0
- jkey-0.1.2/src/jkey/pm/gen.py +33 -0
- jkey-0.1.2/src/jkey/pv/__init__.py +0 -0
- jkey-0.1.2/src/jkey/pv/core.py +290 -0
- jkey-0.1.2/src/jkey/pv/export.py +147 -0
- jkey-0.1.2/src/jkey.egg-info/PKG-INFO +105 -0
- jkey-0.1.2/src/jkey.egg-info/SOURCES.txt +24 -0
- jkey-0.1.2/src/jkey.egg-info/dependency_links.txt +1 -0
- jkey-0.1.2/src/jkey.egg-info/entry_points.txt +2 -0
- jkey-0.1.2/src/jkey.egg-info/requires.txt +1 -0
- jkey-0.1.2/src/jkey.egg-info/top_level.txt +1 -0
- jkey-0.1.2/tests/test_aes.py +106 -0
- jkey-0.1.2/tests/test_generator.py +68 -0
- jkey-0.1.2/tests/test_totp.py +92 -0
jkey-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jkey
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Python library for password management and TOTP verification.
|
|
5
|
+
Author-email: jiaoyuan <imjiaoyuan@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/imjiaoyuan/jkey
|
|
8
|
+
Project-URL: Repository, https://github.com/imjiaoyuan/jkey
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: opencv-python-headless>=4.9.0
|
|
12
|
+
|
|
13
|
+
# jkey
|
|
14
|
+
|
|
15
|
+
Python library for password management and TOTP verification.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv tool install jkey
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or run without installing:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv run jkey --help
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Initialize vault (set master password)
|
|
33
|
+
jkey pv init
|
|
34
|
+
|
|
35
|
+
# Add a 2FA account
|
|
36
|
+
jkey 2fa add github JBSWY3DPEHPK3PXP
|
|
37
|
+
|
|
38
|
+
# Get current TOTP code
|
|
39
|
+
jkey 2fa get github
|
|
40
|
+
|
|
41
|
+
# Import from QR code image
|
|
42
|
+
jkey 2fa qr ./github.jpg
|
|
43
|
+
|
|
44
|
+
# Generate a random password
|
|
45
|
+
jkey pm gen -L 24
|
|
46
|
+
|
|
47
|
+
# Store a password
|
|
48
|
+
jkey pm add my-site
|
|
49
|
+
|
|
50
|
+
# List all stored passwords
|
|
51
|
+
jkey pm ls
|
|
52
|
+
|
|
53
|
+
# Encrypt/decrypt any file
|
|
54
|
+
jkey pv encrypt secret.pdf
|
|
55
|
+
jkey pv decrypt secret.pdf.jkey -o secret.pdf
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Commands
|
|
59
|
+
|
|
60
|
+
| Command | Description |
|
|
61
|
+
|---------|-------------|
|
|
62
|
+
| `jkey 2fa ls [keyword]` | List TOTP accounts and codes |
|
|
63
|
+
| `jkey 2fa get <account>` | Show TOTP code for an account |
|
|
64
|
+
| `jkey 2fa add <name> <secret>` | Add a TOTP account |
|
|
65
|
+
| `jkey 2fa qr <image>` | Import from QR code image |
|
|
66
|
+
| `jkey 2fa rm <account>` | Remove a TOTP account |
|
|
67
|
+
| `jkey pm gen [-L N]` | Generate a random password |
|
|
68
|
+
| `jkey pm ls [keyword]` | List stored passwords |
|
|
69
|
+
| `jkey pm get <name>` | Show a stored password |
|
|
70
|
+
| `jkey pm add <name>` | Store a password (prompts for input) |
|
|
71
|
+
| `jkey pm rm <name>` | Delete a stored password |
|
|
72
|
+
| `jkey pm import <csv>` | Import passwords from CSV (name,password) |
|
|
73
|
+
| `jkey pv init` | Initialize the encrypted vault |
|
|
74
|
+
| `jkey pv unlock` | Unlock the vault |
|
|
75
|
+
| `jkey pv lock` | Lock the vault |
|
|
76
|
+
| `jkey pv set-pw` | Change master password |
|
|
77
|
+
| `jkey pv encrypt <file>` | Encrypt a file |
|
|
78
|
+
| `jkey pv decrypt <file>` | Decrypt a `.jkey` file |
|
|
79
|
+
| `jkey pv export totp` | Export TOTP secrets (re-enters master password) |
|
|
80
|
+
| `jkey pv export passwords` | Export passwords as CSV |
|
|
81
|
+
| `jkey pv export recovery` | Export recovery codes |
|
|
82
|
+
| `jkey pv export qr -o <dir>` | Export QR code images |
|
|
83
|
+
| `jkey pv export all -o <dir>` | Export everything |
|
|
84
|
+
|
|
85
|
+
Set `JKEY_PASS` environment variable to skip the password prompt.
|
|
86
|
+
|
|
87
|
+
## How It Works
|
|
88
|
+
|
|
89
|
+
Data is encrypted with AES-256-CBC + HMAC-SHA256 and stored in `~/.config/jkey/`:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
~/.config/jkey/
|
|
93
|
+
├── totp.jkey # Encrypted TOTP secrets
|
|
94
|
+
├── passwords.jkey # Encrypted passwords
|
|
95
|
+
├── recovery.jkey # Encrypted recovery codes
|
|
96
|
+
└── qr/ # Encrypted QR images
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Back up `~/.config/jkey/` to migrate to another machine.
|
|
100
|
+
|
|
101
|
+
## Dependencies
|
|
102
|
+
|
|
103
|
+
- `opencv-python-headless` — QR code scanning
|
|
104
|
+
|
|
105
|
+
Pure Python, no OpenSSL or libsodium required.
|
jkey-0.1.2/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# jkey
|
|
2
|
+
|
|
3
|
+
Python library for password management and TOTP verification.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv tool install jkey
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run without installing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv run jkey --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Initialize vault (set master password)
|
|
21
|
+
jkey pv init
|
|
22
|
+
|
|
23
|
+
# Add a 2FA account
|
|
24
|
+
jkey 2fa add github JBSWY3DPEHPK3PXP
|
|
25
|
+
|
|
26
|
+
# Get current TOTP code
|
|
27
|
+
jkey 2fa get github
|
|
28
|
+
|
|
29
|
+
# Import from QR code image
|
|
30
|
+
jkey 2fa qr ./github.jpg
|
|
31
|
+
|
|
32
|
+
# Generate a random password
|
|
33
|
+
jkey pm gen -L 24
|
|
34
|
+
|
|
35
|
+
# Store a password
|
|
36
|
+
jkey pm add my-site
|
|
37
|
+
|
|
38
|
+
# List all stored passwords
|
|
39
|
+
jkey pm ls
|
|
40
|
+
|
|
41
|
+
# Encrypt/decrypt any file
|
|
42
|
+
jkey pv encrypt secret.pdf
|
|
43
|
+
jkey pv decrypt secret.pdf.jkey -o secret.pdf
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Commands
|
|
47
|
+
|
|
48
|
+
| Command | Description |
|
|
49
|
+
|---------|-------------|
|
|
50
|
+
| `jkey 2fa ls [keyword]` | List TOTP accounts and codes |
|
|
51
|
+
| `jkey 2fa get <account>` | Show TOTP code for an account |
|
|
52
|
+
| `jkey 2fa add <name> <secret>` | Add a TOTP account |
|
|
53
|
+
| `jkey 2fa qr <image>` | Import from QR code image |
|
|
54
|
+
| `jkey 2fa rm <account>` | Remove a TOTP account |
|
|
55
|
+
| `jkey pm gen [-L N]` | Generate a random password |
|
|
56
|
+
| `jkey pm ls [keyword]` | List stored passwords |
|
|
57
|
+
| `jkey pm get <name>` | Show a stored password |
|
|
58
|
+
| `jkey pm add <name>` | Store a password (prompts for input) |
|
|
59
|
+
| `jkey pm rm <name>` | Delete a stored password |
|
|
60
|
+
| `jkey pm import <csv>` | Import passwords from CSV (name,password) |
|
|
61
|
+
| `jkey pv init` | Initialize the encrypted vault |
|
|
62
|
+
| `jkey pv unlock` | Unlock the vault |
|
|
63
|
+
| `jkey pv lock` | Lock the vault |
|
|
64
|
+
| `jkey pv set-pw` | Change master password |
|
|
65
|
+
| `jkey pv encrypt <file>` | Encrypt a file |
|
|
66
|
+
| `jkey pv decrypt <file>` | Decrypt a `.jkey` file |
|
|
67
|
+
| `jkey pv export totp` | Export TOTP secrets (re-enters master password) |
|
|
68
|
+
| `jkey pv export passwords` | Export passwords as CSV |
|
|
69
|
+
| `jkey pv export recovery` | Export recovery codes |
|
|
70
|
+
| `jkey pv export qr -o <dir>` | Export QR code images |
|
|
71
|
+
| `jkey pv export all -o <dir>` | Export everything |
|
|
72
|
+
|
|
73
|
+
Set `JKEY_PASS` environment variable to skip the password prompt.
|
|
74
|
+
|
|
75
|
+
## How It Works
|
|
76
|
+
|
|
77
|
+
Data is encrypted with AES-256-CBC + HMAC-SHA256 and stored in `~/.config/jkey/`:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
~/.config/jkey/
|
|
81
|
+
├── totp.jkey # Encrypted TOTP secrets
|
|
82
|
+
├── passwords.jkey # Encrypted passwords
|
|
83
|
+
├── recovery.jkey # Encrypted recovery codes
|
|
84
|
+
└── qr/ # Encrypted QR images
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Back up `~/.config/jkey/` to migrate to another machine.
|
|
88
|
+
|
|
89
|
+
## Dependencies
|
|
90
|
+
|
|
91
|
+
- `opencv-python-headless` — QR code scanning
|
|
92
|
+
|
|
93
|
+
Pure Python, no OpenSSL or libsodium required.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "jkey"
|
|
3
|
+
version = "0.1.2"
|
|
4
|
+
description = "Python library for password management and TOTP verification."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
authors = [{name = "jiaoyuan", email = "imjiaoyuan@gmail.com"}]
|
|
8
|
+
license = {text = "MIT"}
|
|
9
|
+
dependencies = ["opencv-python-headless>=4.9.0"]
|
|
10
|
+
|
|
11
|
+
[project.urls]
|
|
12
|
+
Homepage = "https://github.com/imjiaoyuan/jkey"
|
|
13
|
+
Repository = "https://github.com/imjiaoyuan/jkey"
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
jkey = "jkey.cli:main"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["setuptools>=64"]
|
|
20
|
+
build-backend = "setuptools.build_meta"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.packages.find]
|
|
23
|
+
where = ["src"]
|
|
24
|
+
|
|
25
|
+
[dependency-groups]
|
|
26
|
+
dev = ["pytest>=8", "ruff>=0.11"]
|
|
27
|
+
|
|
28
|
+
[tool.ruff]
|
|
29
|
+
line-length = 120
|
|
30
|
+
target-version = "py310"
|
|
31
|
+
|
|
32
|
+
[tool.ruff.lint]
|
|
33
|
+
select = ["E", "F", "W", "I"]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
testpaths = ["tests"]
|
jkey-0.1.2/setup.cfg
ADDED
|
File without changes
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
import hmac
|
|
4
|
+
import os
|
|
5
|
+
import struct
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from jkey.pv.core import load_recovery, load_totp, save_recovery, save_totp
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _hotp(secret: bytes, counter: int, digits: int = 6) -> str:
|
|
12
|
+
msg = struct.pack(">Q", counter)
|
|
13
|
+
h = hmac.new(secret, msg, hashlib.sha1).digest()
|
|
14
|
+
offset = h[-1] & 0xf
|
|
15
|
+
truncated = struct.unpack(">I", h[offset:offset+4])[0] & 0x7fffffff
|
|
16
|
+
return str(truncated % (10 ** digits)).zfill(digits)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _b32_decode(s: str) -> bytes:
|
|
20
|
+
s = s.upper().replace(" ", "")
|
|
21
|
+
remainder = len(s) % 8
|
|
22
|
+
if remainder:
|
|
23
|
+
s += "=" * (8 - remainder)
|
|
24
|
+
return base64.b32decode(s)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _validate_b32_secret(s: str) -> bool:
|
|
28
|
+
try:
|
|
29
|
+
_b32_decode(s)
|
|
30
|
+
return True
|
|
31
|
+
except Exception:
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def totp(secret_key: str, digits: int = 6, interval: int = 30) -> str:
|
|
36
|
+
secret = _b32_decode(secret_key)
|
|
37
|
+
counter = int(time.time()) // interval
|
|
38
|
+
return _hotp(secret, counter, digits)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _import_recovery_file(account: str, recovery_path: str | None):
|
|
42
|
+
if not recovery_path:
|
|
43
|
+
return
|
|
44
|
+
if not os.path.exists(recovery_path):
|
|
45
|
+
print(f"Warning: Recovery file not found: {recovery_path}")
|
|
46
|
+
return
|
|
47
|
+
try:
|
|
48
|
+
with open(recovery_path, "r", encoding="utf-8") as f:
|
|
49
|
+
codes = [line.strip() for line in f if line.strip()]
|
|
50
|
+
if codes:
|
|
51
|
+
data = load_recovery()
|
|
52
|
+
if data is None:
|
|
53
|
+
return
|
|
54
|
+
data[account] = codes
|
|
55
|
+
save_recovery(data)
|
|
56
|
+
print(f"Imported {len(codes)} recovery codes for {account}")
|
|
57
|
+
except OSError as e:
|
|
58
|
+
print(f"Warning: Could not read recovery file: {e}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def list_accounts(keyword: str | None = None):
|
|
62
|
+
data = load_totp()
|
|
63
|
+
if data is None:
|
|
64
|
+
return
|
|
65
|
+
if not data:
|
|
66
|
+
print("No 2FA accounts found.")
|
|
67
|
+
return
|
|
68
|
+
keys = sorted(data.keys())
|
|
69
|
+
if keyword:
|
|
70
|
+
keys = [k for k in keys if keyword.lower() in k.lower()]
|
|
71
|
+
if not keys:
|
|
72
|
+
print(f"No accounts matching '{keyword}'.")
|
|
73
|
+
return
|
|
74
|
+
for acc_id in keys:
|
|
75
|
+
secret = data[acc_id]
|
|
76
|
+
try:
|
|
77
|
+
print(f"{acc_id}: {totp(secret)}")
|
|
78
|
+
except Exception as e:
|
|
79
|
+
print(f"Error processing '{acc_id}': {e}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def show_code(account: str):
|
|
83
|
+
data = load_totp()
|
|
84
|
+
if data is None:
|
|
85
|
+
return
|
|
86
|
+
if account not in data:
|
|
87
|
+
print(f"Error: Account '{account}' not found.")
|
|
88
|
+
return
|
|
89
|
+
secret = data[account]
|
|
90
|
+
try:
|
|
91
|
+
print(f"{account}: {totp(secret)}")
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(f"Error processing '{account}': {e}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def add_account(name: str, secret: str, recovery_path: str | None = None):
|
|
97
|
+
if not _validate_b32_secret(secret):
|
|
98
|
+
print(f"Error: Invalid base32 secret for '{name}'.")
|
|
99
|
+
return
|
|
100
|
+
data = load_totp()
|
|
101
|
+
if data is None:
|
|
102
|
+
return
|
|
103
|
+
data[name] = secret
|
|
104
|
+
save_totp(data)
|
|
105
|
+
print(f"Added 2FA account: {name}")
|
|
106
|
+
_import_recovery_file(name, recovery_path)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def remove_account(account: str):
|
|
110
|
+
data = load_totp()
|
|
111
|
+
if data is None:
|
|
112
|
+
return
|
|
113
|
+
if account not in data:
|
|
114
|
+
print(f"Error: Account '{account}' not found.")
|
|
115
|
+
return
|
|
116
|
+
del data[account]
|
|
117
|
+
save_totp(data)
|
|
118
|
+
|
|
119
|
+
rc = load_recovery()
|
|
120
|
+
if rc and account in rc:
|
|
121
|
+
del rc[account]
|
|
122
|
+
save_recovery(rc)
|
|
123
|
+
|
|
124
|
+
print(f"Removed 2FA account: {account}")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import os
|
|
3
|
+
from urllib.parse import parse_qs, unquote, urlparse
|
|
4
|
+
|
|
5
|
+
import cv2
|
|
6
|
+
|
|
7
|
+
from jkey.pv.core import load_totp, save_qr_image, save_totp
|
|
8
|
+
|
|
9
|
+
_core = importlib.import_module("jkey.2fa.core")
|
|
10
|
+
_import_recovery_file = _core._import_recovery_file
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def scan_and_add(image_path: str, recovery_path: str | None = None):
|
|
14
|
+
if not os.path.exists(image_path):
|
|
15
|
+
print(f"Error: File not found: {image_path}")
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
img = cv2.imread(image_path)
|
|
19
|
+
if img is None:
|
|
20
|
+
print(f"Error: Could not read image: {image_path}")
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
detector = cv2.QRCodeDetector()
|
|
24
|
+
data, _, _ = detector.detectAndDecode(img)
|
|
25
|
+
if not data:
|
|
26
|
+
print("Error: No QR code found in the image.")
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
parsed = urlparse(data)
|
|
30
|
+
if parsed.scheme != "otpauth":
|
|
31
|
+
print(f"Error: Not a valid otpauth:// URL: {data}")
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
params = parse_qs(parsed.query)
|
|
35
|
+
secret = params.get("secret", [None])[0]
|
|
36
|
+
if not secret:
|
|
37
|
+
print("Error: No secret found in QR code.")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
path = unquote(parsed.path).lstrip("/")
|
|
41
|
+
issuer = params.get("issuer", [None])[0]
|
|
42
|
+
|
|
43
|
+
if issuer and path.startswith(issuer + ":"):
|
|
44
|
+
name = path[len(issuer) + 1:]
|
|
45
|
+
elif issuer and ":" in path:
|
|
46
|
+
name = path
|
|
47
|
+
else:
|
|
48
|
+
name = path
|
|
49
|
+
|
|
50
|
+
if not name:
|
|
51
|
+
name = issuer or "unknown"
|
|
52
|
+
|
|
53
|
+
data = load_totp()
|
|
54
|
+
if data is None:
|
|
55
|
+
return
|
|
56
|
+
data[name] = secret
|
|
57
|
+
save_totp(data)
|
|
58
|
+
print(f"Added 2FA account: {name}")
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
save_qr_image(name, cv2.imencode(".jpg", img)[1].tobytes())
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
_import_recovery_file(name, recovery_path)
|
|
File without changes
|