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.
Files changed (28) hide show
  1. formseal_decrypt-0.2.0/LICENSE +21 -0
  2. formseal_decrypt-0.2.0/PKG-INFO +150 -0
  3. formseal_decrypt-0.2.0/README.md +116 -0
  4. formseal_decrypt-0.2.0/formseal_decrypt.egg-info/PKG-INFO +150 -0
  5. formseal_decrypt-0.2.0/formseal_decrypt.egg-info/SOURCES.txt +26 -0
  6. formseal_decrypt-0.2.0/formseal_decrypt.egg-info/dependency_links.txt +1 -0
  7. formseal_decrypt-0.2.0/formseal_decrypt.egg-info/entry_points.txt +2 -0
  8. formseal_decrypt-0.2.0/formseal_decrypt.egg-info/requires.txt +2 -0
  9. formseal_decrypt-0.2.0/formseal_decrypt.egg-info/top_level.txt +1 -0
  10. formseal_decrypt-0.2.0/fsd/cmd.py +14 -0
  11. formseal_decrypt-0.2.0/fsd/commands/config/config.py +117 -0
  12. formseal_decrypt-0.2.0/fsd/commands/connect/connect.py +122 -0
  13. formseal_decrypt-0.2.0/fsd/commands/decrypt/decrypt.py +117 -0
  14. formseal_decrypt-0.2.0/fsd/commands/general/about.py +19 -0
  15. formseal_decrypt-0.2.0/fsd/commands/general/help.py +60 -0
  16. formseal_decrypt-0.2.0/fsd/commands/general/version.py +7 -0
  17. formseal_decrypt-0.2.0/fsd/formats/__init__.py +26 -0
  18. formseal_decrypt-0.2.0/fsd/formats/formatter.py +13 -0
  19. formseal_decrypt-0.2.0/fsd/formats/json.py +15 -0
  20. formseal_decrypt-0.2.0/fsd/formats/jsonl.py +16 -0
  21. formseal_decrypt-0.2.0/fsd/fsd.py +61 -0
  22. formseal_decrypt-0.2.0/fsd/security/keys.py +104 -0
  23. formseal_decrypt-0.2.0/fsd/ui/__init__.py +10 -0
  24. formseal_decrypt-0.2.0/fsd/ui/bodies.py +50 -0
  25. formseal_decrypt-0.2.0/fsd/ui/headers.py +16 -0
  26. formseal_decrypt-0.2.0/fsd/ui/styles.py +45 -0
  27. formseal_decrypt-0.2.0/pyproject.toml +34 -0
  28. 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,2 @@
1
+ [console_scripts]
2
+ fsd = fsd.fsd:main
@@ -0,0 +1,2 @@
1
+ keyring>=25.0
2
+ pynacl>=1.5.0
@@ -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,7 @@
1
+ # commands/general/version.py
2
+
3
+ VERSION = "0.2.0"
4
+
5
+
6
+ def run():
7
+ print(f"v{VERSION}")
@@ -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,13 @@
1
+ # formats/formatter.py
2
+
3
+ from abc import ABC, abstractmethod
4
+ from pathlib import Path
5
+
6
+
7
+ class Formatter(ABC):
8
+ name: str = ""
9
+ extension: str = ""
10
+
11
+ @abstractmethod
12
+ def write(self, data: list[dict], path: Path):
13
+ pass
@@ -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
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+