formseal-decrypt 0.2.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.
- formseal_decrypt-0.2.0/LICENSE +21 -0
- formseal_decrypt-0.2.0/PKG-INFO +150 -0
- formseal_decrypt-0.2.0/README.md +116 -0
- formseal_decrypt-0.2.0/formseal_decrypt.egg-info/PKG-INFO +150 -0
- formseal_decrypt-0.2.0/formseal_decrypt.egg-info/SOURCES.txt +26 -0
- formseal_decrypt-0.2.0/formseal_decrypt.egg-info/dependency_links.txt +1 -0
- formseal_decrypt-0.2.0/formseal_decrypt.egg-info/entry_points.txt +2 -0
- formseal_decrypt-0.2.0/formseal_decrypt.egg-info/requires.txt +2 -0
- formseal_decrypt-0.2.0/formseal_decrypt.egg-info/top_level.txt +1 -0
- formseal_decrypt-0.2.0/fsd/cmd.py +14 -0
- formseal_decrypt-0.2.0/fsd/commands/config/config.py +117 -0
- formseal_decrypt-0.2.0/fsd/commands/connect/connect.py +122 -0
- formseal_decrypt-0.2.0/fsd/commands/decrypt/decrypt.py +117 -0
- formseal_decrypt-0.2.0/fsd/commands/general/about.py +19 -0
- formseal_decrypt-0.2.0/fsd/commands/general/help.py +60 -0
- formseal_decrypt-0.2.0/fsd/commands/general/version.py +7 -0
- formseal_decrypt-0.2.0/fsd/formats/__init__.py +26 -0
- formseal_decrypt-0.2.0/fsd/formats/formatter.py +13 -0
- formseal_decrypt-0.2.0/fsd/formats/json.py +15 -0
- formseal_decrypt-0.2.0/fsd/formats/jsonl.py +16 -0
- formseal_decrypt-0.2.0/fsd/fsd.py +61 -0
- formseal_decrypt-0.2.0/fsd/security/keys.py +104 -0
- formseal_decrypt-0.2.0/fsd/ui/__init__.py +10 -0
- formseal_decrypt-0.2.0/fsd/ui/bodies.py +50 -0
- formseal_decrypt-0.2.0/fsd/ui/headers.py +16 -0
- formseal_decrypt-0.2.0/fsd/ui/styles.py +45 -0
- formseal_decrypt-0.2.0/pyproject.toml +34 -0
- formseal_decrypt-0.2.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 useFormseal contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: formseal-decrypt
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: CLI tool for decrypting formseal ciphertexts
|
|
5
|
+
Author: grayguava
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 useFormseal contributors
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Requires-Python: >=3.8
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: keyring>=25.0
|
|
32
|
+
Requires-Dist: pynacl>=1.5.0
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
<p align="center">
|
|
36
|
+
<img src="fsd.png" alt="formseal-decrypt">
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
<p align="center">
|
|
40
|
+
<img src="https://img.shields.io/pypi/v/formseal-decrypt?style=flat&label=pypi&labelColor=1e293b&color=3776ab">
|
|
41
|
+
<img src="https://img.shields.io/github/actions/workflow/status/useFormseal/decrypt/publish.yml">
|
|
42
|
+
<img src="https://img.shields.io/badge/license-MIT-fc8181?style=flat&labelColor=1e293b">
|
|
43
|
+
<img src="https://img.shields.io/badge/formseal-ecosystem-10b981?style=flat&labelColor=1e293b">
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
<p align="center">
|
|
47
|
+
Decrypt formseal ciphertexts locally.
|
|
48
|
+
</p>
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
formseal-decrypt decrypts form submissions downloaded by formseal-fetch. Nothing is decrypted in transit or on the server — only the holder of the private key can read submissions.
|
|
53
|
+
|
|
54
|
+
formseal-decrypt is not a hosted service or dashboard. It is a CLI decryption utility.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
**Via pipx (recommended)**
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pipx install formseal-decrypt
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Via pip**
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install formseal-decrypt
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Quick start
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
fsd connect
|
|
78
|
+
fsd decrypt
|
|
79
|
+
fsd status
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## How it works
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
Browser (formseal-embed)
|
|
88
|
+
│
|
|
89
|
+
▼ (encrypted submissions)
|
|
90
|
+
Your server / endpoint
|
|
91
|
+
│
|
|
92
|
+
▼ (fsf fetch)
|
|
93
|
+
ciphertexts.jsonl ──► Your PC
|
|
94
|
+
│
|
|
95
|
+
▼ (fsd decrypt)
|
|
96
|
+
formseal.decrypted.jsonl (or .json for pretty output)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Your backend stores opaque ciphertext only. `fsf fetch` downloads it. `fsd decrypt` decrypts it offline with your private key.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Commands
|
|
104
|
+
|
|
105
|
+
| Command | Description |
|
|
106
|
+
|---------|-------------|
|
|
107
|
+
| `fsd` | Show about / info |
|
|
108
|
+
| `fsd connect` | Configure source, destination, private key, and format |
|
|
109
|
+
| `fsd decrypt` | Decrypt ciphertexts |
|
|
110
|
+
| `fsd status` | Show configuration |
|
|
111
|
+
| `fsd disconnect` | Clear credentials |
|
|
112
|
+
| `fsd disconnect --wipe` | Clear everything including messages |
|
|
113
|
+
|
|
114
|
+
Run `fsd --help` for all options.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Output Formats
|
|
119
|
+
|
|
120
|
+
During `fsd connect`, you can choose the output format:
|
|
121
|
+
|
|
122
|
+
- **JSON Lines** (`jsonl`) — One JSON object per line
|
|
123
|
+
- **JSON** (`json`) — Pretty-printed JSON array
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Security
|
|
128
|
+
|
|
129
|
+
Your private key never leaves your machine. formseal-decrypt:
|
|
130
|
+
|
|
131
|
+
- Stores credentials in your OS keychain (Windows Credential Manager / macOS Keychain / Linux Secret Service)
|
|
132
|
+
- Decrypts locally only
|
|
133
|
+
- Sends no telemetry, has no analytics
|
|
134
|
+
- Skips already-decrypted messages automatically
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Documentation
|
|
139
|
+
|
|
140
|
+
- [SECURITY.md](./.github/SECURITY.md) — Security policy
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
Please star the repo if you find formseal-decrypt useful.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="fsd.png" alt="formseal-decrypt">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="https://img.shields.io/pypi/v/formseal-decrypt?style=flat&label=pypi&labelColor=1e293b&color=3776ab">
|
|
7
|
+
<img src="https://img.shields.io/github/actions/workflow/status/useFormseal/decrypt/publish.yml">
|
|
8
|
+
<img src="https://img.shields.io/badge/license-MIT-fc8181?style=flat&labelColor=1e293b">
|
|
9
|
+
<img src="https://img.shields.io/badge/formseal-ecosystem-10b981?style=flat&labelColor=1e293b">
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
Decrypt formseal ciphertexts locally.
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
formseal-decrypt decrypts form submissions downloaded by formseal-fetch. Nothing is decrypted in transit or on the server — only the holder of the private key can read submissions.
|
|
19
|
+
|
|
20
|
+
formseal-decrypt is not a hosted service or dashboard. It is a CLI decryption utility.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
**Via pipx (recommended)**
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pipx install formseal-decrypt
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Via pip**
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install formseal-decrypt
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
fsd connect
|
|
44
|
+
fsd decrypt
|
|
45
|
+
fsd status
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## How it works
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
Browser (formseal-embed)
|
|
54
|
+
│
|
|
55
|
+
▼ (encrypted submissions)
|
|
56
|
+
Your server / endpoint
|
|
57
|
+
│
|
|
58
|
+
▼ (fsf fetch)
|
|
59
|
+
ciphertexts.jsonl ──► Your PC
|
|
60
|
+
│
|
|
61
|
+
▼ (fsd decrypt)
|
|
62
|
+
formseal.decrypted.jsonl (or .json for pretty output)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Your backend stores opaque ciphertext only. `fsf fetch` downloads it. `fsd decrypt` decrypts it offline with your private key.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Commands
|
|
70
|
+
|
|
71
|
+
| Command | Description |
|
|
72
|
+
|---------|-------------|
|
|
73
|
+
| `fsd` | Show about / info |
|
|
74
|
+
| `fsd connect` | Configure source, destination, private key, and format |
|
|
75
|
+
| `fsd decrypt` | Decrypt ciphertexts |
|
|
76
|
+
| `fsd status` | Show configuration |
|
|
77
|
+
| `fsd disconnect` | Clear credentials |
|
|
78
|
+
| `fsd disconnect --wipe` | Clear everything including messages |
|
|
79
|
+
|
|
80
|
+
Run `fsd --help` for all options.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Output Formats
|
|
85
|
+
|
|
86
|
+
During `fsd connect`, you can choose the output format:
|
|
87
|
+
|
|
88
|
+
- **JSON Lines** (`jsonl`) — One JSON object per line
|
|
89
|
+
- **JSON** (`json`) — Pretty-printed JSON array
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Security
|
|
94
|
+
|
|
95
|
+
Your private key never leaves your machine. formseal-decrypt:
|
|
96
|
+
|
|
97
|
+
- Stores credentials in your OS keychain (Windows Credential Manager / macOS Keychain / Linux Secret Service)
|
|
98
|
+
- Decrypts locally only
|
|
99
|
+
- Sends no telemetry, has no analytics
|
|
100
|
+
- Skips already-decrypted messages automatically
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Documentation
|
|
105
|
+
|
|
106
|
+
- [SECURITY.md](./.github/SECURITY.md) — Security policy
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
Please star the repo if you find formseal-decrypt useful.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: formseal-decrypt
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: CLI tool for decrypting formseal ciphertexts
|
|
5
|
+
Author: grayguava
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 useFormseal contributors
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Requires-Python: >=3.8
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: keyring>=25.0
|
|
32
|
+
Requires-Dist: pynacl>=1.5.0
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
<p align="center">
|
|
36
|
+
<img src="fsd.png" alt="formseal-decrypt">
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
<p align="center">
|
|
40
|
+
<img src="https://img.shields.io/pypi/v/formseal-decrypt?style=flat&label=pypi&labelColor=1e293b&color=3776ab">
|
|
41
|
+
<img src="https://img.shields.io/github/actions/workflow/status/useFormseal/decrypt/publish.yml">
|
|
42
|
+
<img src="https://img.shields.io/badge/license-MIT-fc8181?style=flat&labelColor=1e293b">
|
|
43
|
+
<img src="https://img.shields.io/badge/formseal-ecosystem-10b981?style=flat&labelColor=1e293b">
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
<p align="center">
|
|
47
|
+
Decrypt formseal ciphertexts locally.
|
|
48
|
+
</p>
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
formseal-decrypt decrypts form submissions downloaded by formseal-fetch. Nothing is decrypted in transit or on the server — only the holder of the private key can read submissions.
|
|
53
|
+
|
|
54
|
+
formseal-decrypt is not a hosted service or dashboard. It is a CLI decryption utility.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
**Via pipx (recommended)**
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pipx install formseal-decrypt
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Via pip**
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install formseal-decrypt
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Quick start
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
fsd connect
|
|
78
|
+
fsd decrypt
|
|
79
|
+
fsd status
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## How it works
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
Browser (formseal-embed)
|
|
88
|
+
│
|
|
89
|
+
▼ (encrypted submissions)
|
|
90
|
+
Your server / endpoint
|
|
91
|
+
│
|
|
92
|
+
▼ (fsf fetch)
|
|
93
|
+
ciphertexts.jsonl ──► Your PC
|
|
94
|
+
│
|
|
95
|
+
▼ (fsd decrypt)
|
|
96
|
+
formseal.decrypted.jsonl (or .json for pretty output)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Your backend stores opaque ciphertext only. `fsf fetch` downloads it. `fsd decrypt` decrypts it offline with your private key.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Commands
|
|
104
|
+
|
|
105
|
+
| Command | Description |
|
|
106
|
+
|---------|-------------|
|
|
107
|
+
| `fsd` | Show about / info |
|
|
108
|
+
| `fsd connect` | Configure source, destination, private key, and format |
|
|
109
|
+
| `fsd decrypt` | Decrypt ciphertexts |
|
|
110
|
+
| `fsd status` | Show configuration |
|
|
111
|
+
| `fsd disconnect` | Clear credentials |
|
|
112
|
+
| `fsd disconnect --wipe` | Clear everything including messages |
|
|
113
|
+
|
|
114
|
+
Run `fsd --help` for all options.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Output Formats
|
|
119
|
+
|
|
120
|
+
During `fsd connect`, you can choose the output format:
|
|
121
|
+
|
|
122
|
+
- **JSON Lines** (`jsonl`) — One JSON object per line
|
|
123
|
+
- **JSON** (`json`) — Pretty-printed JSON array
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Security
|
|
128
|
+
|
|
129
|
+
Your private key never leaves your machine. formseal-decrypt:
|
|
130
|
+
|
|
131
|
+
- Stores credentials in your OS keychain (Windows Credential Manager / macOS Keychain / Linux Secret Service)
|
|
132
|
+
- Decrypts locally only
|
|
133
|
+
- Sends no telemetry, has no analytics
|
|
134
|
+
- Skips already-decrypted messages automatically
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Documentation
|
|
139
|
+
|
|
140
|
+
- [SECURITY.md](./.github/SECURITY.md) — Security policy
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
Please star the repo if you find formseal-decrypt useful.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
formseal_decrypt.egg-info/PKG-INFO
|
|
5
|
+
formseal_decrypt.egg-info/SOURCES.txt
|
|
6
|
+
formseal_decrypt.egg-info/dependency_links.txt
|
|
7
|
+
formseal_decrypt.egg-info/entry_points.txt
|
|
8
|
+
formseal_decrypt.egg-info/requires.txt
|
|
9
|
+
formseal_decrypt.egg-info/top_level.txt
|
|
10
|
+
fsd/cmd.py
|
|
11
|
+
fsd/fsd.py
|
|
12
|
+
fsd/commands/config/config.py
|
|
13
|
+
fsd/commands/connect/connect.py
|
|
14
|
+
fsd/commands/decrypt/decrypt.py
|
|
15
|
+
fsd/commands/general/about.py
|
|
16
|
+
fsd/commands/general/help.py
|
|
17
|
+
fsd/commands/general/version.py
|
|
18
|
+
fsd/formats/__init__.py
|
|
19
|
+
fsd/formats/formatter.py
|
|
20
|
+
fsd/formats/json.py
|
|
21
|
+
fsd/formats/jsonl.py
|
|
22
|
+
fsd/security/keys.py
|
|
23
|
+
fsd/ui/__init__.py
|
|
24
|
+
fsd/ui/bodies.py
|
|
25
|
+
fsd/ui/headers.py
|
|
26
|
+
fsd/ui/styles.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fsd
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# fsd/cmd.py
|
|
2
|
+
# Command registry
|
|
3
|
+
|
|
4
|
+
from fsd.commands.config.config import run_status, run_disconnect
|
|
5
|
+
from fsd.commands.connect.connect import run as run_connect
|
|
6
|
+
from fsd.commands.decrypt.decrypt import run as run_decrypt
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
COMMANDS = {
|
|
10
|
+
"connect": ("Configure source, destination, and private key", lambda a: run_connect(a)),
|
|
11
|
+
"decrypt": ("Decrypt ciphertexts", lambda a: run_decrypt(a)),
|
|
12
|
+
"status": ("Show configuration status", lambda a: run_status()),
|
|
13
|
+
"disconnect": ("Clear all credentials", lambda a: run_disconnect(a)),
|
|
14
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# commands/config/config.py
|
|
2
|
+
# Config management
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from fsd.ui import br, fail, ok, info, warn, G, W, D, C, Y, R, HEAD, header
|
|
9
|
+
from fsd.security import keys
|
|
10
|
+
from fsd.formats import FORMATTERS
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
CONFIG_DIR = Path.home() / ".config" / "formseal-decrypt"
|
|
14
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_config():
|
|
18
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
if CONFIG_FILE.exists():
|
|
20
|
+
return json.loads(CONFIG_FILE.read_text())
|
|
21
|
+
return {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def save_config(cfg):
|
|
25
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
CONFIG_FILE.write_text(json.dumps(cfg, indent=2))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_token():
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_namespace():
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_status():
|
|
38
|
+
cfg = load_config()
|
|
39
|
+
|
|
40
|
+
br()
|
|
41
|
+
header()
|
|
42
|
+
br()
|
|
43
|
+
|
|
44
|
+
print(f" {D}Configuration Status:{R}")
|
|
45
|
+
br()
|
|
46
|
+
|
|
47
|
+
source = cfg.get("source")
|
|
48
|
+
if not source:
|
|
49
|
+
warn("Not configured. Run: fsd connect")
|
|
50
|
+
br()
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
def row(label, value, color=W):
|
|
54
|
+
print(f" {D}{label:<26}{R}{color}{value}{R}")
|
|
55
|
+
|
|
56
|
+
row("Source:", source)
|
|
57
|
+
|
|
58
|
+
destination = cfg.get("destination")
|
|
59
|
+
row("Destination:", destination or "(not set)", W if destination else D)
|
|
60
|
+
|
|
61
|
+
output_format = cfg.get("format", "jsonl")
|
|
62
|
+
formatter = FORMATTERS.get(output_format)
|
|
63
|
+
format_display = formatter.name if formatter else output_format
|
|
64
|
+
row("Format:", format_display, G)
|
|
65
|
+
|
|
66
|
+
private_key = keys.load_private_key()
|
|
67
|
+
if private_key:
|
|
68
|
+
row("Private Key:", keys.private_key_location(), G)
|
|
69
|
+
else:
|
|
70
|
+
row("Private Key:", "Not set", D)
|
|
71
|
+
|
|
72
|
+
br()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def run_disconnect(args=None):
|
|
76
|
+
args = args or []
|
|
77
|
+
wipe = "--wipe" in args
|
|
78
|
+
|
|
79
|
+
if wipe:
|
|
80
|
+
br()
|
|
81
|
+
print(f"{Y}THIS WILL DELETE EVERYTHING.{R}")
|
|
82
|
+
print(f"Config, private key, AND decrypted messages will be deleted.")
|
|
83
|
+
else:
|
|
84
|
+
br()
|
|
85
|
+
print(f"{Y}This will delete config and private key.{R}")
|
|
86
|
+
print(f"Decrypted messages will NOT be affected.")
|
|
87
|
+
br()
|
|
88
|
+
sys.stdout.write(f" Continue? [y/N]: ")
|
|
89
|
+
sys.stdout.flush()
|
|
90
|
+
confirm = input().strip().lower()
|
|
91
|
+
|
|
92
|
+
if confirm != "y":
|
|
93
|
+
br()
|
|
94
|
+
info("Cancelled.")
|
|
95
|
+
br()
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
cfg = load_config()
|
|
99
|
+
|
|
100
|
+
if wipe:
|
|
101
|
+
destination = cfg.get("destination")
|
|
102
|
+
if destination:
|
|
103
|
+
dest_path = Path(destination)
|
|
104
|
+
if dest_path.exists():
|
|
105
|
+
dest_path.unlink()
|
|
106
|
+
|
|
107
|
+
if CONFIG_FILE.exists():
|
|
108
|
+
CONFIG_FILE.unlink()
|
|
109
|
+
|
|
110
|
+
keys.clear_all()
|
|
111
|
+
|
|
112
|
+
br()
|
|
113
|
+
if wipe:
|
|
114
|
+
ok("Disconnected. Everything wiped.")
|
|
115
|
+
else:
|
|
116
|
+
ok("Disconnected. Config and private key cleared.")
|
|
117
|
+
br()
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Connect command
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from fsd.ui import br, fail, ok, info, warn, G, W, D, C, Y, O, R, HEAD, OK, header
|
|
7
|
+
from fsd.commands.config.config import load_config, save_config
|
|
8
|
+
from fsd.security import keys
|
|
9
|
+
from fsd.formats import FORMATTERS, get_format_names
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _parse_args(args):
|
|
13
|
+
parsed = {}
|
|
14
|
+
for arg in args:
|
|
15
|
+
if ":" not in arg:
|
|
16
|
+
continue
|
|
17
|
+
key, value = arg.split(":", 1)
|
|
18
|
+
parsed[key] = value
|
|
19
|
+
return parsed
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run(args):
|
|
23
|
+
parsed = _parse_args(args)
|
|
24
|
+
|
|
25
|
+
cfg = load_config()
|
|
26
|
+
if cfg.get("source"):
|
|
27
|
+
fail(f"Already configured.\nRun 'fsd disconnect' first.")
|
|
28
|
+
|
|
29
|
+
print()
|
|
30
|
+
header("setup")
|
|
31
|
+
print()
|
|
32
|
+
|
|
33
|
+
cfg = load_config()
|
|
34
|
+
|
|
35
|
+
source = parsed.get("source")
|
|
36
|
+
if not source:
|
|
37
|
+
try:
|
|
38
|
+
sys.stdout.write(f" Source File (ciphertexts): ")
|
|
39
|
+
sys.stdout.flush()
|
|
40
|
+
source = input().strip()
|
|
41
|
+
if not source:
|
|
42
|
+
fail("Source file is required")
|
|
43
|
+
if not source.endswith(".jsonl"):
|
|
44
|
+
source = source + ".jsonl"
|
|
45
|
+
except KeyboardInterrupt:
|
|
46
|
+
br()
|
|
47
|
+
info("Cancelled.")
|
|
48
|
+
br()
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
destination = parsed.get("destination")
|
|
52
|
+
if not destination:
|
|
53
|
+
try:
|
|
54
|
+
sys.stdout.write(f" Destination Directory: ")
|
|
55
|
+
sys.stdout.flush()
|
|
56
|
+
destination = input().strip()
|
|
57
|
+
if not destination:
|
|
58
|
+
destination = "."
|
|
59
|
+
except KeyboardInterrupt:
|
|
60
|
+
br()
|
|
61
|
+
info("Cancelled.")
|
|
62
|
+
br()
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
available_formats = get_format_names()
|
|
66
|
+
output_format = parsed.get("format")
|
|
67
|
+
if not output_format:
|
|
68
|
+
try:
|
|
69
|
+
sys.stdout.write(f" Output Format [{available_formats}]: ")
|
|
70
|
+
sys.stdout.flush()
|
|
71
|
+
output_format = input().strip()
|
|
72
|
+
if not output_format:
|
|
73
|
+
output_format = "jsonl"
|
|
74
|
+
except KeyboardInterrupt:
|
|
75
|
+
br()
|
|
76
|
+
info("Cancelled.")
|
|
77
|
+
br()
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
if output_format.lower() not in FORMATTERS:
|
|
81
|
+
fail(f"Invalid format: {output_format}. Available: {available_formats}")
|
|
82
|
+
|
|
83
|
+
output_format = output_format.lower()
|
|
84
|
+
|
|
85
|
+
private_key = parsed.get("private-key")
|
|
86
|
+
if not private_key:
|
|
87
|
+
try:
|
|
88
|
+
sys.stdout.write(f" Private Key: ")
|
|
89
|
+
sys.stdout.flush()
|
|
90
|
+
private_key = input().strip()
|
|
91
|
+
except KeyboardInterrupt:
|
|
92
|
+
br()
|
|
93
|
+
info("Cancelled.")
|
|
94
|
+
br()
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
if not private_key:
|
|
98
|
+
fail("Private key is required")
|
|
99
|
+
|
|
100
|
+
source_path = Path(source).expanduser().resolve()
|
|
101
|
+
dest_dir = Path(destination).expanduser().resolve()
|
|
102
|
+
|
|
103
|
+
if not source_path.exists():
|
|
104
|
+
fail(f"Source file not found: {source}")
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
except Exception:
|
|
109
|
+
fail("Could not create destination directory. Check permissions.")
|
|
110
|
+
|
|
111
|
+
cfg["source"] = str(source_path)
|
|
112
|
+
cfg["destination"] = str(dest_dir)
|
|
113
|
+
cfg["format"] = output_format
|
|
114
|
+
save_config(cfg)
|
|
115
|
+
|
|
116
|
+
keys.save_private_key(private_key)
|
|
117
|
+
|
|
118
|
+
print()
|
|
119
|
+
print(f"{G}{OK}{R} Saved!")
|
|
120
|
+
print()
|
|
121
|
+
print(f" Run {W}fsd decrypt{R} to decrypt messages")
|
|
122
|
+
print()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# decrypt command
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import nacl.public
|
|
7
|
+
import nacl.encoding
|
|
8
|
+
|
|
9
|
+
from fsd.ui import br, fail, ok, info, warn, G, W, D, C, Y, R, HEAD, header
|
|
10
|
+
from fsd.commands.config.config import load_config
|
|
11
|
+
from fsd.security import keys
|
|
12
|
+
from fsd.formats import get_formatter, get_extension
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run(args):
|
|
16
|
+
cfg = load_config()
|
|
17
|
+
|
|
18
|
+
source = cfg.get("source")
|
|
19
|
+
if not source:
|
|
20
|
+
fail("Not configured. Run: fsd connect")
|
|
21
|
+
|
|
22
|
+
destination = cfg.get("destination")
|
|
23
|
+
if not destination:
|
|
24
|
+
fail("Destination not set. Run: fsd connect")
|
|
25
|
+
|
|
26
|
+
output_format = cfg.get("format", "jsonl")
|
|
27
|
+
formatter = get_formatter(output_format)
|
|
28
|
+
|
|
29
|
+
private_key = keys.load_private_key()
|
|
30
|
+
if not private_key:
|
|
31
|
+
fail("Private key not set. Run: fsd connect")
|
|
32
|
+
|
|
33
|
+
source_path = Path(source)
|
|
34
|
+
dest_dir = Path(destination)
|
|
35
|
+
ext = get_extension(output_format)
|
|
36
|
+
dest_path = dest_dir / f"formseal.decrypted.{ext}"
|
|
37
|
+
|
|
38
|
+
if not source_path.exists():
|
|
39
|
+
fail(f"Source file not found: {source}")
|
|
40
|
+
|
|
41
|
+
br()
|
|
42
|
+
header("decrypt")
|
|
43
|
+
br()
|
|
44
|
+
|
|
45
|
+
def row(label, value, color=W):
|
|
46
|
+
print(f" {D}{label:<26}{R}{color}{value}{R}")
|
|
47
|
+
|
|
48
|
+
row("Source:", str(source_path))
|
|
49
|
+
row("Destination:", str(dest_path))
|
|
50
|
+
row("Format:", output_format)
|
|
51
|
+
|
|
52
|
+
br()
|
|
53
|
+
|
|
54
|
+
private_key_bytes = _decode_base64url(private_key)
|
|
55
|
+
if not private_key_bytes or len(private_key_bytes) != 32:
|
|
56
|
+
fail("Invalid private key. Must be 32-byte base64url.")
|
|
57
|
+
|
|
58
|
+
private_key_box = nacl.public.PrivateKey(private_key_bytes)
|
|
59
|
+
|
|
60
|
+
decrypted = []
|
|
61
|
+
failed = 0
|
|
62
|
+
total = 0
|
|
63
|
+
|
|
64
|
+
with open(source_path, "r", encoding="utf-8") as f:
|
|
65
|
+
for line in f:
|
|
66
|
+
line = line.strip()
|
|
67
|
+
if not line:
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
total += 1
|
|
71
|
+
try:
|
|
72
|
+
decrypted_msg = _decrypt_line(line, private_key_box)
|
|
73
|
+
decrypted.append(decrypted_msg)
|
|
74
|
+
except Exception:
|
|
75
|
+
failed += 1
|
|
76
|
+
|
|
77
|
+
if decrypted:
|
|
78
|
+
formatter.write(decrypted, dest_path)
|
|
79
|
+
|
|
80
|
+
br()
|
|
81
|
+
row("Processed:", total, G)
|
|
82
|
+
row("Decrypted:", len(decrypted), G if len(decrypted) > 0 else D)
|
|
83
|
+
row("Failed:", failed, Y if failed > 0 else D)
|
|
84
|
+
|
|
85
|
+
if failed > 0:
|
|
86
|
+
br()
|
|
87
|
+
warn(f"Some messages could not be decrypted.")
|
|
88
|
+
warn(f"Check that the private key is correct.")
|
|
89
|
+
|
|
90
|
+
br()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _decode_base64url(b64url):
|
|
94
|
+
b64url = b64url.replace("-", "+").replace("_", "/")
|
|
95
|
+
pad = len(b64url) % 4
|
|
96
|
+
if pad:
|
|
97
|
+
b64url += "=" * (4 - pad)
|
|
98
|
+
try:
|
|
99
|
+
import base64
|
|
100
|
+
return base64.b64decode(b64url)
|
|
101
|
+
except Exception:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _decrypt_line(line, private_key_box):
|
|
106
|
+
if not line.startswith("formseal."):
|
|
107
|
+
raise ValueError("Invalid format: missing formseal. prefix")
|
|
108
|
+
|
|
109
|
+
ciphertext_b64url = line[9:]
|
|
110
|
+
ciphertext_bytes = _decode_base64url(ciphertext_b64url)
|
|
111
|
+
|
|
112
|
+
if not ciphertext_bytes:
|
|
113
|
+
raise ValueError("Invalid ciphertext encoding")
|
|
114
|
+
|
|
115
|
+
sealed_box = nacl.public.SealedBox(private_key_box)
|
|
116
|
+
plaintext = sealed_box.decrypt(ciphertext_bytes, encoder=nacl.encoding.RawEncoder)
|
|
117
|
+
return json.loads(plaintext)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# commands/general/about.py
|
|
2
|
+
# About command - shows project info
|
|
3
|
+
|
|
4
|
+
from fsd.ui import br, header, C, G, W, R
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def run():
|
|
8
|
+
br()
|
|
9
|
+
header()
|
|
10
|
+
br()
|
|
11
|
+
|
|
12
|
+
print(f" {W}CLI for decrypting formseal ciphertexts{R}")
|
|
13
|
+
br()
|
|
14
|
+
print(f" Part of the {C}formseal{R} ecosystem")
|
|
15
|
+
br()
|
|
16
|
+
print(f" {G}License: {R} MIT")
|
|
17
|
+
print(f" {G}Maintained by:{R} grayguava")
|
|
18
|
+
print(f" {G}Repository: {R} https://github.com/useFormseal/decrypt")
|
|
19
|
+
br()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# commands/general/help.py
|
|
2
|
+
# Help command - shows all available commands
|
|
3
|
+
|
|
4
|
+
from fsd.ui import br, header, cmd_line, rule
|
|
5
|
+
from fsd.ui.styles import C, G, R, W, GRAY
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _get_help_groups():
|
|
9
|
+
return {
|
|
10
|
+
"Connect": [
|
|
11
|
+
("fsd connect", "configure source, destination, and private key"),
|
|
12
|
+
("fsd disconnect", "clear configuration"),
|
|
13
|
+
("fsd disconnect --wipe", "clear everything including messages"),
|
|
14
|
+
],
|
|
15
|
+
"Decrypt": [
|
|
16
|
+
("fsd decrypt", "decrypt ciphertexts"),
|
|
17
|
+
],
|
|
18
|
+
"Info": [
|
|
19
|
+
("fsd status", "show configuration"),
|
|
20
|
+
("fsd --version", "show version"),
|
|
21
|
+
("fsd --aliases", "list shorthand flags"),
|
|
22
|
+
],
|
|
23
|
+
"Docs": [
|
|
24
|
+
("https://github.com/useFormseal/decrypt", None),
|
|
25
|
+
],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _show_help():
|
|
30
|
+
groups = _get_help_groups()
|
|
31
|
+
br()
|
|
32
|
+
header()
|
|
33
|
+
br()
|
|
34
|
+
|
|
35
|
+
for group, cmds in groups.items():
|
|
36
|
+
print(f" {GRAY}>> {group}{R}")
|
|
37
|
+
print(G + " " + "─" * 44 + R)
|
|
38
|
+
for cmd, desc in cmds:
|
|
39
|
+
if desc:
|
|
40
|
+
print(f" {W}{cmd:<27}{R} {G}{desc}{R}")
|
|
41
|
+
else:
|
|
42
|
+
print(f" {C}{cmd}{R}")
|
|
43
|
+
br()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def run():
|
|
47
|
+
_show_help()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_aliases():
|
|
51
|
+
br()
|
|
52
|
+
header("shorthand aliases")
|
|
53
|
+
br()
|
|
54
|
+
|
|
55
|
+
print(f" {W}Short{R} {G}Canonical{R}")
|
|
56
|
+
print(G + " " + "─" * 44 + R)
|
|
57
|
+
print(f" {W}-s{R} {G}status{R}")
|
|
58
|
+
print(f" {W}-c{R} {G}connect{R}")
|
|
59
|
+
print(f" {W}-d{R} {G}decrypt{R}")
|
|
60
|
+
br()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# formats/__init__.py
|
|
2
|
+
|
|
3
|
+
from .formatter import Formatter
|
|
4
|
+
from .jsonl import JsonlFormatter
|
|
5
|
+
from .json import JsonFormatter
|
|
6
|
+
|
|
7
|
+
FORMATTERS: dict[str, type[Formatter]] = {
|
|
8
|
+
"jsonl": JsonlFormatter,
|
|
9
|
+
"json": JsonFormatter,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_formatter(format_name: str) -> Formatter:
|
|
14
|
+
formatter_class = FORMATTERS.get(format_name)
|
|
15
|
+
if not formatter_class:
|
|
16
|
+
available = ", ".join(FORMATTERS.keys())
|
|
17
|
+
raise ValueError(f"Unknown format: {format_name}. Available: {available}")
|
|
18
|
+
return formatter_class()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_format_names() -> str:
|
|
22
|
+
return ", ".join(f"{fmt.name} ({fmt})" for fmt in FORMATTERS.values())
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_extension(format_name: str) -> str:
|
|
26
|
+
return FORMATTERS[format_name].extension
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# formats/json.py
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from .formatter import Formatter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class JsonFormatter(Formatter):
|
|
9
|
+
name = "Pretty JSON"
|
|
10
|
+
extension = "json"
|
|
11
|
+
|
|
12
|
+
def write(self, data: list[dict], path: Path):
|
|
13
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
14
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
15
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# formats/jsonl.py
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from .formatter import Formatter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class JsonlFormatter(Formatter):
|
|
9
|
+
name = "JSON Lines"
|
|
10
|
+
extension = "jsonl"
|
|
11
|
+
|
|
12
|
+
def write(self, data: list[dict], path: Path):
|
|
13
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
14
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
15
|
+
for msg in data:
|
|
16
|
+
f.write(json.dumps(msg, ensure_ascii=False) + "\n")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Main entry point
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
script_dir = Path(__file__).absolute()
|
|
8
|
+
project_root = script_dir.parent.parent
|
|
9
|
+
sys.path.insert(0, str(project_root))
|
|
10
|
+
os.chdir(project_root)
|
|
11
|
+
|
|
12
|
+
from fsd.cmd import COMMANDS
|
|
13
|
+
from fsd.general.aliases import resolve
|
|
14
|
+
from fsd.general.errors import unknown_command, handle_interrupt, handle_exception
|
|
15
|
+
|
|
16
|
+
from fsd.commands.general import about as cmd_about
|
|
17
|
+
from fsd.commands.general import help as cmd_help
|
|
18
|
+
from fsd.commands.general import version as cmd_version
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main():
|
|
22
|
+
if len(sys.argv) < 2:
|
|
23
|
+
cmd_about.run()
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
args = resolve(sys.argv[1:])
|
|
27
|
+
cmd = args[0].lower()
|
|
28
|
+
cmd_args = args[1:]
|
|
29
|
+
|
|
30
|
+
if cmd == "--help":
|
|
31
|
+
cmd_help.run()
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
if cmd == "--about":
|
|
35
|
+
cmd_about.run()
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
if cmd == "--version":
|
|
39
|
+
cmd_version.run()
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
if cmd == "--aliases":
|
|
43
|
+
cmd_help.run_aliases()
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
if cmd not in COMMANDS:
|
|
47
|
+
unknown_command(cmd)
|
|
48
|
+
|
|
49
|
+
_, handler = COMMANDS[cmd]
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
handler(cmd_args)
|
|
53
|
+
except KeyboardInterrupt:
|
|
54
|
+
handle_interrupt()
|
|
55
|
+
sys.exit(130)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
handle_exception(e)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
main()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Credential storage (keyring + JSON fallback)
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import base64
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import keyring
|
|
9
|
+
HAS_KEYRING = True
|
|
10
|
+
except ImportError:
|
|
11
|
+
HAS_KEYRING = False
|
|
12
|
+
|
|
13
|
+
SERVICE = "formseal-decrypt"
|
|
14
|
+
|
|
15
|
+
CONFIG_DIR = Path.home() / ".config" / "formseal-decrypt"
|
|
16
|
+
SECRETS_FILE = CONFIG_DIR / "secrets.json"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _ensure_config_dir():
|
|
20
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load_secrets() -> dict:
|
|
24
|
+
if not SECRETS_FILE.exists():
|
|
25
|
+
return {}
|
|
26
|
+
try:
|
|
27
|
+
return json.loads(SECRETS_FILE.read_text())
|
|
28
|
+
except:
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _save_secrets(secrets: dict):
|
|
33
|
+
if not secrets:
|
|
34
|
+
if SECRETS_FILE.exists():
|
|
35
|
+
SECRETS_FILE.unlink()
|
|
36
|
+
return
|
|
37
|
+
_ensure_config_dir()
|
|
38
|
+
SECRETS_FILE.write_text(json.dumps(secrets, indent=2))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def save_private_key(private_key: str) -> bool:
|
|
42
|
+
"""Save private key to OS keyring. Falls back to JSON if keyring fails."""
|
|
43
|
+
if HAS_KEYRING:
|
|
44
|
+
try:
|
|
45
|
+
keyring.set_password(SERVICE, "private-key", private_key)
|
|
46
|
+
return True
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
secrets = _load_secrets()
|
|
51
|
+
secrets["private-key"] = base64.b64encode(private_key.encode()).decode()
|
|
52
|
+
_save_secrets(secrets)
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def load_private_key() -> str | None:
|
|
57
|
+
"""Load private key from OS keyring. Falls back to JSON if not in keyring."""
|
|
58
|
+
if HAS_KEYRING:
|
|
59
|
+
try:
|
|
60
|
+
key = keyring.get_password(SERVICE, "private-key")
|
|
61
|
+
if key:
|
|
62
|
+
return key
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
secrets = _load_secrets()
|
|
67
|
+
encoded = secrets.get("private-key")
|
|
68
|
+
if encoded:
|
|
69
|
+
return base64.b64decode(encoded.encode()).decode().strip()
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def delete_private_key():
|
|
74
|
+
"""Delete private key from keyring."""
|
|
75
|
+
if HAS_KEYRING:
|
|
76
|
+
try:
|
|
77
|
+
keyring.delete_password(SERVICE, "private-key")
|
|
78
|
+
return
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
secrets = _load_secrets()
|
|
83
|
+
secrets.pop("private-key", None)
|
|
84
|
+
_save_secrets(secrets)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def private_key_location() -> str:
|
|
88
|
+
"""Check where private key is stored."""
|
|
89
|
+
if HAS_KEYRING:
|
|
90
|
+
try:
|
|
91
|
+
if keyring.get_password(SERVICE, "private-key"):
|
|
92
|
+
return "OS Keychain"
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
secrets = _load_secrets()
|
|
97
|
+
if "private-key" in secrets:
|
|
98
|
+
return "Config File"
|
|
99
|
+
return "Not set"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def clear_all():
|
|
103
|
+
"""Clear all secrets."""
|
|
104
|
+
delete_private_key()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# ui/__init__.py
|
|
2
|
+
# Re-export from styles and bodies
|
|
3
|
+
|
|
4
|
+
from fsd.ui.styles import (
|
|
5
|
+
RESET, BOLD, DIM, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, GRAY,
|
|
6
|
+
O, S, G, C, Y, M, W, D, R, HEAD, OK, TICK, CROSS, ERR
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from fsd.ui.bodies import br, badge, fail, row, cmd_line, code, link, ok, info, warn
|
|
10
|
+
from fsd.ui.headers import header, rule
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# ui/bodies.py
|
|
2
|
+
|
|
3
|
+
from fsd.ui.styles import C, D, G, O, R, S, W, Y, BOLD, RED
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def br():
|
|
7
|
+
print()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def badge(label, color):
|
|
11
|
+
return f"{color}{BOLD} {label} {R}"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def fail(msg):
|
|
15
|
+
br()
|
|
16
|
+
print(f"{badge('❌', RED)} {msg}")
|
|
17
|
+
br()
|
|
18
|
+
raise SystemExit(1)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def row(icon, label, value):
|
|
22
|
+
pad = 12
|
|
23
|
+
label = (label + " " * pad)[:pad]
|
|
24
|
+
print(f"{S}{icon}{R} {D}{label}{R} {W}{value}{R}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def cmd_line(command, desc):
|
|
28
|
+
pad = 34
|
|
29
|
+
command = (command + " " * pad)[:pad]
|
|
30
|
+
print(f" {W}{command}{R}{G}{desc}{R}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def code(msg):
|
|
34
|
+
print(f" {O}{msg}{R}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def link(msg):
|
|
38
|
+
print(f" {O}{msg}{R}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def ok(msg):
|
|
42
|
+
print(f" {G}✨{R} {msg}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def info(msg):
|
|
46
|
+
print(f" {O}{msg}{R}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def warn(msg):
|
|
50
|
+
print(f"{Y}⚠️ {R}{msg}")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# ui/headers.py
|
|
2
|
+
# Header and rule functions
|
|
3
|
+
|
|
4
|
+
from fsd.ui.styles import C, G, R, W, Y, HEAD
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def header(title=""):
|
|
8
|
+
if title:
|
|
9
|
+
print(f"{C} \u250c\u2500 {HEAD} {R}{W}formseal-decrypt{R} {Y}{title}{R}")
|
|
10
|
+
else:
|
|
11
|
+
print(f"{C} \u250c\u2500 {HEAD} {R}{W}formseal-decrypt{R}")
|
|
12
|
+
print(G + " " + "\u2500" * 52 + R)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def rule():
|
|
16
|
+
print(G + " " + "\u2500" * 52 + R)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# ui/styles.py
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
if os.name == "nt":
|
|
7
|
+
try:
|
|
8
|
+
os.system("chcp 65001 >nul")
|
|
9
|
+
except:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
14
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
15
|
+
except:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
RESET = "\x1b[0m"
|
|
19
|
+
BOLD = "\x1b[1m"
|
|
20
|
+
DIM = "\x1b[2m"
|
|
21
|
+
|
|
22
|
+
RED = "\x1b[31m"
|
|
23
|
+
GREEN = "\x1b[32m"
|
|
24
|
+
YELLOW = "\x1b[33m"
|
|
25
|
+
BLUE = "\x1b[34m"
|
|
26
|
+
MAGENTA= "\x1b[35m"
|
|
27
|
+
CYAN = "\x1b[36m"
|
|
28
|
+
WHITE = "\x1b[37m"
|
|
29
|
+
GRAY = "\x1b[90m"
|
|
30
|
+
|
|
31
|
+
O = "\x1b[38;5;208m"
|
|
32
|
+
S = "\x1b[38;5;112m"
|
|
33
|
+
G = "\x1b[38;5;244m"
|
|
34
|
+
C = "\x1b[38;5;108m"
|
|
35
|
+
Y = "\x1b[38;5;103m"
|
|
36
|
+
M = "\x1b[38;5;141m"
|
|
37
|
+
W = WHITE + BOLD
|
|
38
|
+
D = DIM
|
|
39
|
+
R = RESET
|
|
40
|
+
|
|
41
|
+
HEAD = "🙈"
|
|
42
|
+
OK = "✨"
|
|
43
|
+
TICK = "✔️"
|
|
44
|
+
CROSS = "❌"
|
|
45
|
+
ERR = "😵💫"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=65.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "formseal-decrypt"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "CLI tool for decrypting formseal ciphertexts"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {file = "LICENSE"}
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "grayguava"}
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"keyring>=25.0",
|
|
17
|
+
"pynacl>=1.5.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
fsd = "fsd.fsd:main"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools]
|
|
24
|
+
packages = [
|
|
25
|
+
"fsd",
|
|
26
|
+
"fsd.ui",
|
|
27
|
+
"fsd.commands",
|
|
28
|
+
"fsd.commands.general",
|
|
29
|
+
"fsd.commands.config",
|
|
30
|
+
"fsd.commands.connect",
|
|
31
|
+
"fsd.commands.decrypt",
|
|
32
|
+
"fsd.security",
|
|
33
|
+
"fsd.formats",
|
|
34
|
+
]
|