stealth-message-cli 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,212 @@
1
+ """First-use setup wizard (alias + passphrase → RSA-4096 keypair).
2
+
3
+ Collects alias and passphrase interactively via prompt_toolkit, generates
4
+ an RSA-4096 keypair, saves it to disk, and returns the credentials needed
5
+ to start the chat session.
6
+
7
+ Usage (called by __main__.py)::
8
+
9
+ alias, armored_private, passphrase = await run_setup()
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ from typing import Optional
16
+
17
+ from prompt_toolkit import PromptSession
18
+ from prompt_toolkit.formatted_text import HTML
19
+ from prompt_toolkit.styles import Style
20
+ from prompt_toolkit.validation import ValidationError, Validator
21
+ from rich.console import Console
22
+ from rich.panel import Panel
23
+ from rich.progress import Progress, SpinnerColumn, TextColumn
24
+ from rich.rule import Rule
25
+ from rich.text import Text
26
+
27
+ from stealth_cli import config
28
+ from stealth_cli.crypto.keys import generate_keypair, get_fingerprint
29
+
30
+ console = Console()
31
+
32
+ _STYLE = Style.from_dict(
33
+ {
34
+ "prompt": "bold cyan",
35
+ "label": "ansicyan",
36
+ }
37
+ )
38
+
39
+ # --------------------------------------------------------------------------- #
40
+ # Validators #
41
+ # --------------------------------------------------------------------------- #
42
+
43
+
44
+ class _NonEmptyValidator(Validator):
45
+ def __init__(self, field: str, min_len: int = 1) -> None:
46
+ self._field = field
47
+ self._min_len = min_len
48
+
49
+ def validate(self, document): # type: ignore[override]
50
+ if len(document.text.strip()) < self._min_len:
51
+ raise ValidationError(
52
+ message=f"{self._field} must be at least {self._min_len} character(s)",
53
+ cursor_position=len(document.text),
54
+ )
55
+
56
+
57
+ class _AliasValidator(_NonEmptyValidator):
58
+ def __init__(self) -> None:
59
+ super().__init__("Alias", min_len=1)
60
+
61
+ def validate(self, document): # type: ignore[override]
62
+ super().validate(document)
63
+ if len(document.text.strip()) > 64:
64
+ raise ValidationError(
65
+ message="Alias must be 64 characters or fewer",
66
+ cursor_position=len(document.text),
67
+ )
68
+
69
+
70
+ class _PassphraseValidator(Validator):
71
+ MIN_LEN = 8
72
+
73
+ def validate(self, document): # type: ignore[override]
74
+ if len(document.text) < self.MIN_LEN:
75
+ raise ValidationError(
76
+ message=f"Passphrase must be at least {self.MIN_LEN} characters",
77
+ cursor_position=len(document.text),
78
+ )
79
+
80
+
81
+ # --------------------------------------------------------------------------- #
82
+ # Public entry point #
83
+ # --------------------------------------------------------------------------- #
84
+
85
+
86
+ async def run_setup() -> tuple[str, str, str]:
87
+ """Interactive first-use wizard.
88
+
89
+ Prompts for alias and passphrase, generates an RSA-4096 keypair,
90
+ persists it to disk, and returns ``(alias, armored_private, passphrase)``.
91
+ """
92
+ _print_welcome()
93
+
94
+ session: PromptSession[str] = PromptSession(style=_STYLE)
95
+
96
+ alias = await _prompt_alias(session)
97
+ passphrase = await _prompt_passphrase(session)
98
+
99
+ armored_private, armored_public = await _generate_with_spinner(alias, passphrase)
100
+
101
+ config.save_keypair(armored_private, armored_public, alias)
102
+
103
+ _print_success(alias, armored_public)
104
+
105
+ return alias, armored_private, passphrase
106
+
107
+
108
+ # --------------------------------------------------------------------------- #
109
+ # UI helpers #
110
+ # --------------------------------------------------------------------------- #
111
+
112
+
113
+ def _print_welcome() -> None:
114
+ console.print()
115
+ console.print(
116
+ Panel(
117
+ Text.assemble(
118
+ ("stealth-message", "bold white"),
119
+ "\n",
120
+ ("End-to-end encrypted PGP chat — no server, no accounts", "dim"),
121
+ ),
122
+ border_style="cyan",
123
+ padding=(1, 4),
124
+ )
125
+ )
126
+ console.print(
127
+ Rule("[cyan]First-time setup[/cyan]", style="dim"),
128
+ )
129
+ console.print()
130
+
131
+
132
+ async def _prompt_alias(session: PromptSession[str]) -> str:
133
+ """Prompt for a display alias (1–64 chars)."""
134
+ console.print("[cyan]Choose a display name[/cyan] (visible to peers, max 64 chars)")
135
+ alias: str = await session.prompt_async(
136
+ HTML("<prompt>Alias: </prompt>"),
137
+ validator=_AliasValidator(),
138
+ validate_while_typing=False,
139
+ )
140
+ return alias.strip()
141
+
142
+
143
+ async def _prompt_passphrase(session: PromptSession[str]) -> str:
144
+ """Prompt for passphrase with confirmation."""
145
+ console.print()
146
+ console.print(
147
+ "[cyan]Choose a passphrase[/cyan] — protects your private key on disk"
148
+ )
149
+ console.print("[dim]Minimum 8 characters. You will be asked for it on every start.[/dim]")
150
+
151
+ while True:
152
+ passphrase: str = await session.prompt_async(
153
+ HTML("<prompt>Passphrase: </prompt>"),
154
+ is_password=True,
155
+ validator=_PassphraseValidator(),
156
+ validate_while_typing=False,
157
+ )
158
+ confirm: str = await session.prompt_async(
159
+ HTML("<prompt>Confirm passphrase: </prompt>"),
160
+ is_password=True,
161
+ )
162
+ if passphrase == confirm:
163
+ break
164
+ console.print("[red]Passphrases do not match. Try again.[/red]")
165
+
166
+ return passphrase
167
+
168
+
169
+ async def _generate_with_spinner(alias: str, passphrase: str) -> tuple[str, str]:
170
+ """Generate RSA-4096 keypair in a thread pool with a spinner."""
171
+ console.print()
172
+
173
+ with Progress(
174
+ SpinnerColumn(),
175
+ TextColumn("[cyan]{task.description}"),
176
+ transient=True,
177
+ console=console,
178
+ ) as progress:
179
+ task = progress.add_task("Generating RSA-4096 keypair…", total=None)
180
+
181
+ loop = asyncio.get_event_loop()
182
+ armored_private, armored_public = await loop.run_in_executor(
183
+ None, generate_keypair, alias, passphrase
184
+ )
185
+ progress.remove_task(task)
186
+
187
+ return armored_private, armored_public
188
+
189
+
190
+ def _print_success(alias: str, armored_public: str) -> None:
191
+ fp = get_fingerprint(armored_public)
192
+
193
+ console.print("[green]✓[/green] Keypair generated and saved.")
194
+ console.print()
195
+ console.print(
196
+ Panel(
197
+ Text.assemble(
198
+ ("Alias: ", "bold"),
199
+ (alias, "cyan"),
200
+ "\n",
201
+ ("Fingerprint: ", "bold"),
202
+ (fp, "yellow"),
203
+ ),
204
+ title="[bold]Your identity[/bold]",
205
+ border_style="green",
206
+ padding=(1, 3),
207
+ )
208
+ )
209
+ console.print(
210
+ "[dim]Share your fingerprint out-of-band so peers can verify your identity.[/dim]"
211
+ )
212
+ console.print()
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: stealth-message-cli
3
+ Version: 0.1.0
4
+ Summary: Terminal client for stealth-message — end-to-end encrypted PGP chat
5
+ License: GPL-3.0-only
6
+ Project-URL: Homepage, https://syberiancode.com/stealth-message
7
+ Project-URL: Repository, https://github.com/syberiancode/stealth-message
8
+ Project-URL: Issues, https://github.com/syberiancode/stealth-message/issues
9
+ Keywords: chat,encryption,pgp,privacy,end-to-end
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Communications :: Chat
18
+ Classifier: Topic :: Security :: Cryptography
19
+ Requires-Python: <3.13,>=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: pgpy>=0.6.0
22
+ Requires-Dist: websockets>=12.0
23
+ Requires-Dist: rich>=13.0
24
+ Requires-Dist: prompt_toolkit>=3.0
25
+ Requires-Dist: platformdirs>=4.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0; extra == "dev"
28
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
29
+ Requires-Dist: black>=24.0; extra == "dev"
30
+ Requires-Dist: ruff>=0.4; extra == "dev"
31
+ Requires-Dist: mypy>=1.10; extra == "dev"
32
+
33
+ # stealth-message CLI
34
+
35
+ End-to-end encrypted PGP chat. No central server. No accounts. No content metadata.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ curl -fsSL https://syberiancode.com/stealth-message/install.sh | bash
41
+ ```
42
+
43
+ **Windows (PowerShell):**
44
+ ```powershell
45
+ powershell -c "irm https://syberiancode.com/stealth-message/install.ps1 | iex"
46
+ ```
47
+
48
+ **Or install directly with pip:**
49
+ ```bash
50
+ pip install stealth-message-cli
51
+ ```
52
+
53
+ ## Requirements
54
+
55
+ - Python 3.10, 3.11, or 3.12
56
+
57
+ ## Usage
58
+
59
+ ```bash
60
+ stealth-cli
61
+ ```
62
+
63
+ ## Security
64
+
65
+ - RSA-4096 keypair per user
66
+ - Sign-then-encrypt on send, decrypt-then-verify on receive
67
+ - Private key is passphrase-protected on disk
68
+ - Wire encoding: ASCII-armored PGP → Base64 URL-safe
69
+
70
+ ## License
71
+
72
+ GPL-3.0. See [LICENSE](https://github.com/syberiancode/stealth-message/blob/main/LICENSE).
@@ -0,0 +1,18 @@
1
+ stealth_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ stealth_cli/__main__.py,sha256=V1-olLs3_5aOYIdLb0q5jbZH1IMyrPlxXQ-BAkgG0qw,22331
3
+ stealth_cli/config.py,sha256=Us73UNRL6qPLlhhIn_M8tbxorCnUUtDssoFUCu6vbwU,4119
4
+ stealth_cli/exceptions.py,sha256=yblXvvVrFzkQIza7kwoM01N2S8BFa-HDFsS1XoJcVoU,870
5
+ stealth_cli/crypto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ stealth_cli/crypto/keys.py,sha256=M6C1wECQLEKgp96wJ7xnim5Ke9Kej0TohiflW1Ltqnw,3932
7
+ stealth_cli/crypto/messages.py,sha256=HKTL-vYxvfflV4fe88SWlL_jTdULbi9mlgZMimjta7s,4911
8
+ stealth_cli/network/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ stealth_cli/network/client.py,sha256=7zAEn5oc_AqfDNb73vyUVP5bCgA5XgFw4knu0N0CxZc,20738
10
+ stealth_cli/network/server.py,sha256=qtU6jkBR2lWcUpYHGmx7L4uLIPsa96qYw04RPhDar1o,27169
11
+ stealth_cli/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ stealth_cli/ui/chat.py,sha256=FpTbB6ejgbimW1SFVxE4pJtdpnTOYfy2qmUPRCAqfmw,40954
13
+ stealth_cli/ui/setup.py,sha256=uUhyXzF_5EGCrJVMSkXdCMi4pahvJXDBJaYRFYYgVBw,6775
14
+ stealth_message_cli-0.1.0.dist-info/METADATA,sha256=TD1i1u0z-tow6Vqnskw91P0SYbZnvf2TN4e81tE2ogQ,2137
15
+ stealth_message_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
16
+ stealth_message_cli-0.1.0.dist-info/entry_points.txt,sha256=tdlk9LpnPBPOp_OzWOuAtUprcQxTUspvYmP2YBrWU6o,58
17
+ stealth_message_cli-0.1.0.dist-info/top_level.txt,sha256=AgMse2uy3u79YKPdSSjvZcNtUJCftRHWJGd8b7ZXuvM,12
18
+ stealth_message_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ stealth-cli = stealth_cli.__main__:main
@@ -0,0 +1 @@
1
+ stealth_cli