bwssh 0.1.1__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.
bwssh-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.3
2
+ Name: bwssh
3
+ Version: 0.1.1
4
+ Summary: Bitwarden-backed SSH agent for Linux
5
+ Requires-Dist: click>=8.3.1
6
+ Requires-Dist: cryptography>=46.0.4
7
+ Requires-Dist: dbus-fast>=4.0.0
8
+ Requires-Dist: textual>=7.5.0
9
+ Requires-Dist: pygobject>=3.42.0,<3.50 ; extra == 'gui'
10
+ Requires-Python: >=3.12
11
+ Provides-Extra: gui
12
+ Description-Content-Type: text/markdown
13
+
14
+ # bwssh
15
+
16
+ Bitwarden-backed SSH agent for Linux. Store your SSH keys in Bitwarden and use
17
+ them seamlessly with any SSH client.
18
+
19
+ ## Features
20
+
21
+ - **Bitwarden integration**: SSH keys stored securely in your Bitwarden vault
22
+ - **Standard SSH agent**: Works with `ssh`, `git`, and any SSH client
23
+ - **Systemd integration**: Runs as a user service, starts on login
24
+ - **Forwarding protection**: Blocks remote servers from using your keys
25
+ - **Optional polkit prompts**: Desktop authorization popups (disabled by default)
26
+
27
+ ## Requirements
28
+
29
+ - Linux with systemd user services
30
+ - Python 3.12+
31
+ - Bitwarden CLI (`bw`) installed and logged in
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ uv sync
37
+ ```
38
+
39
+ ## Bitwarden CLI
40
+
41
+ Install the Bitwarden CLI (`bw`) and log in before using bwssh. See
42
+ https://bitwarden.com/help/cli/ for installation instructions.
43
+
44
+ ```bash
45
+ bw --version
46
+ bw login
47
+ ```
48
+
49
+ ## Quick start
50
+
51
+ ```bash
52
+ uv run bwssh install --user-systemd
53
+ uv run bwssh start
54
+ uv run bwssh unlock
55
+ ```
56
+
57
+ ```bash
58
+ export SSH_AUTH_SOCK=${XDG_RUNTIME_DIR}/bwssh/agent.sock
59
+ ssh -T git@github.com
60
+ ```
61
+
62
+ ## Configuration
63
+
64
+ Config file: `~/.config/bwssh/config.toml`
65
+
66
+ ### Quick Setup (Recommended)
67
+
68
+ The easiest way to configure bwssh is to use the init command:
69
+
70
+ ```bash
71
+ # First, unlock Bitwarden
72
+ export BW_SESSION=$(bw unlock --raw)
73
+
74
+ # Then run init to auto-discover SSH keys
75
+ bwssh config init
76
+ ```
77
+
78
+ This will find all SSH keys in your Bitwarden vault and create a config file.
79
+
80
+ ### Manual Setup
81
+
82
+ If you prefer to configure manually, first find your SSH key IDs:
83
+
84
+ ```bash
85
+ bw list items | jq -r '.[] | select(.sshKey != null) | "\(.id) \(.name)"'
86
+ ```
87
+
88
+ Then create `~/.config/bwssh/config.toml`:
89
+
90
+ ```toml
91
+ [bitwarden]
92
+ bw_path = "/full/path/to/bw" # Use 'which bw' to find this
93
+ item_ids = [
94
+ "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", # your-key-name
95
+ ]
96
+ ```
97
+
98
+ ### Full Config Example
99
+
100
+ ```toml
101
+ [daemon]
102
+ log_level = "INFO"
103
+
104
+ [bitwarden]
105
+ bw_path = "/usr/bin/bw"
106
+ item_ids = [
107
+ "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
108
+ ]
109
+
110
+ [auth]
111
+ # Polkit authorization prompts (default: disabled)
112
+ require_polkit = false
113
+
114
+ # Block forwarded agent requests (recommended)
115
+ deny_forwarded_by_default = true
116
+
117
+ [ssh]
118
+ allow_ed25519 = true
119
+ allow_ecdsa = true
120
+ allow_rsa = true
121
+ ```
122
+
123
+ ### Environment Variables
124
+
125
+ - `BWSSH_RUNTIME_DIR`: Override socket directory
126
+ - `BWSSH_LOG_LEVEL`: Override log level
127
+ - `BW_SESSION`: Bitwarden session key (auto-detected by `bwssh unlock`)
128
+
129
+ ## Security
130
+
131
+ ### Default Mode
132
+
133
+ By default, bwssh allows all local signing requests without prompts. Security comes from:
134
+
135
+ - **Auto-lock on sleep**: Keys are cleared when your laptop sleeps (enabled by default)
136
+ - **Forwarded agent blocking**: Remote servers can't use your keys
137
+ - **Manual lock**: Run `bwssh lock` when stepping away
138
+
139
+ ### Polkit Prompts (Optional)
140
+
141
+ For extra security, enable polkit to show desktop prompts for each signing request:
142
+
143
+ ```toml
144
+ [auth]
145
+ require_polkit = true
146
+ ```
147
+
148
+ This requires installing the polkit policy:
149
+
150
+ ```bash
151
+ bwssh install --polkit | sudo tee /usr/share/polkit-1/actions/io.github.reidond.bwssh.policy > /dev/null
152
+ ```
153
+
154
+ See `docs/` for detailed polkit setup instructions.
155
+
156
+ ## CLI Commands
157
+
158
+ ```bash
159
+ # Daemon control
160
+ bwssh start # Start the agent daemon
161
+ bwssh stop # Stop the agent daemon
162
+ bwssh status # Show daemon status
163
+
164
+ # Key management
165
+ bwssh unlock # Unlock vault and load keys
166
+ bwssh lock # Lock agent and clear keys
167
+ bwssh sync # Reload keys from Bitwarden
168
+ bwssh keys # List loaded SSH keys
169
+
170
+ # Configuration
171
+ bwssh config init # Auto-discover SSH keys and create config
172
+ bwssh config show # Show current configuration
173
+
174
+ # Installation
175
+ bwssh install --user-systemd # Install systemd user service
176
+ bwssh install --polkit # Print polkit policy file
177
+ ```
178
+
179
+ ## Documentation
180
+
181
+ Full documentation lives in `docs/` and can be served locally:
182
+
183
+ ```bash
184
+ cd docs
185
+ bun install
186
+ bun run dev
187
+ ```
188
+
189
+ ## Development
190
+
191
+ ```bash
192
+ uv run ruff check .
193
+ uv run ruff format .
194
+ uv run mypy src tests
195
+ uv run pytest
196
+ ```
bwssh-0.1.1/README.md ADDED
@@ -0,0 +1,183 @@
1
+ # bwssh
2
+
3
+ Bitwarden-backed SSH agent for Linux. Store your SSH keys in Bitwarden and use
4
+ them seamlessly with any SSH client.
5
+
6
+ ## Features
7
+
8
+ - **Bitwarden integration**: SSH keys stored securely in your Bitwarden vault
9
+ - **Standard SSH agent**: Works with `ssh`, `git`, and any SSH client
10
+ - **Systemd integration**: Runs as a user service, starts on login
11
+ - **Forwarding protection**: Blocks remote servers from using your keys
12
+ - **Optional polkit prompts**: Desktop authorization popups (disabled by default)
13
+
14
+ ## Requirements
15
+
16
+ - Linux with systemd user services
17
+ - Python 3.12+
18
+ - Bitwarden CLI (`bw`) installed and logged in
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ uv sync
24
+ ```
25
+
26
+ ## Bitwarden CLI
27
+
28
+ Install the Bitwarden CLI (`bw`) and log in before using bwssh. See
29
+ https://bitwarden.com/help/cli/ for installation instructions.
30
+
31
+ ```bash
32
+ bw --version
33
+ bw login
34
+ ```
35
+
36
+ ## Quick start
37
+
38
+ ```bash
39
+ uv run bwssh install --user-systemd
40
+ uv run bwssh start
41
+ uv run bwssh unlock
42
+ ```
43
+
44
+ ```bash
45
+ export SSH_AUTH_SOCK=${XDG_RUNTIME_DIR}/bwssh/agent.sock
46
+ ssh -T git@github.com
47
+ ```
48
+
49
+ ## Configuration
50
+
51
+ Config file: `~/.config/bwssh/config.toml`
52
+
53
+ ### Quick Setup (Recommended)
54
+
55
+ The easiest way to configure bwssh is to use the init command:
56
+
57
+ ```bash
58
+ # First, unlock Bitwarden
59
+ export BW_SESSION=$(bw unlock --raw)
60
+
61
+ # Then run init to auto-discover SSH keys
62
+ bwssh config init
63
+ ```
64
+
65
+ This will find all SSH keys in your Bitwarden vault and create a config file.
66
+
67
+ ### Manual Setup
68
+
69
+ If you prefer to configure manually, first find your SSH key IDs:
70
+
71
+ ```bash
72
+ bw list items | jq -r '.[] | select(.sshKey != null) | "\(.id) \(.name)"'
73
+ ```
74
+
75
+ Then create `~/.config/bwssh/config.toml`:
76
+
77
+ ```toml
78
+ [bitwarden]
79
+ bw_path = "/full/path/to/bw" # Use 'which bw' to find this
80
+ item_ids = [
81
+ "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", # your-key-name
82
+ ]
83
+ ```
84
+
85
+ ### Full Config Example
86
+
87
+ ```toml
88
+ [daemon]
89
+ log_level = "INFO"
90
+
91
+ [bitwarden]
92
+ bw_path = "/usr/bin/bw"
93
+ item_ids = [
94
+ "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
95
+ ]
96
+
97
+ [auth]
98
+ # Polkit authorization prompts (default: disabled)
99
+ require_polkit = false
100
+
101
+ # Block forwarded agent requests (recommended)
102
+ deny_forwarded_by_default = true
103
+
104
+ [ssh]
105
+ allow_ed25519 = true
106
+ allow_ecdsa = true
107
+ allow_rsa = true
108
+ ```
109
+
110
+ ### Environment Variables
111
+
112
+ - `BWSSH_RUNTIME_DIR`: Override socket directory
113
+ - `BWSSH_LOG_LEVEL`: Override log level
114
+ - `BW_SESSION`: Bitwarden session key (auto-detected by `bwssh unlock`)
115
+
116
+ ## Security
117
+
118
+ ### Default Mode
119
+
120
+ By default, bwssh allows all local signing requests without prompts. Security comes from:
121
+
122
+ - **Auto-lock on sleep**: Keys are cleared when your laptop sleeps (enabled by default)
123
+ - **Forwarded agent blocking**: Remote servers can't use your keys
124
+ - **Manual lock**: Run `bwssh lock` when stepping away
125
+
126
+ ### Polkit Prompts (Optional)
127
+
128
+ For extra security, enable polkit to show desktop prompts for each signing request:
129
+
130
+ ```toml
131
+ [auth]
132
+ require_polkit = true
133
+ ```
134
+
135
+ This requires installing the polkit policy:
136
+
137
+ ```bash
138
+ bwssh install --polkit | sudo tee /usr/share/polkit-1/actions/io.github.reidond.bwssh.policy > /dev/null
139
+ ```
140
+
141
+ See `docs/` for detailed polkit setup instructions.
142
+
143
+ ## CLI Commands
144
+
145
+ ```bash
146
+ # Daemon control
147
+ bwssh start # Start the agent daemon
148
+ bwssh stop # Stop the agent daemon
149
+ bwssh status # Show daemon status
150
+
151
+ # Key management
152
+ bwssh unlock # Unlock vault and load keys
153
+ bwssh lock # Lock agent and clear keys
154
+ bwssh sync # Reload keys from Bitwarden
155
+ bwssh keys # List loaded SSH keys
156
+
157
+ # Configuration
158
+ bwssh config init # Auto-discover SSH keys and create config
159
+ bwssh config show # Show current configuration
160
+
161
+ # Installation
162
+ bwssh install --user-systemd # Install systemd user service
163
+ bwssh install --polkit # Print polkit policy file
164
+ ```
165
+
166
+ ## Documentation
167
+
168
+ Full documentation lives in `docs/` and can be served locally:
169
+
170
+ ```bash
171
+ cd docs
172
+ bun install
173
+ bun run dev
174
+ ```
175
+
176
+ ## Development
177
+
178
+ ```bash
179
+ uv run ruff check .
180
+ uv run ruff format .
181
+ uv run mypy src tests
182
+ uv run pytest
183
+ ```
@@ -0,0 +1,102 @@
1
+ #:schema false
2
+
3
+ [project]
4
+ name = "bwssh"
5
+ version = "0.1.1"
6
+ description = "Bitwarden-backed SSH agent for Linux"
7
+ readme = "README.md"
8
+ requires-python = ">=3.12"
9
+ dependencies = [
10
+ "click>=8.3.1",
11
+ "cryptography>=46.0.4",
12
+ "dbus-fast>=4.0.0",
13
+ "textual>=7.5.0",
14
+ ]
15
+
16
+ [project.optional-dependencies]
17
+ gui = ["PyGObject>=3.42.0,<3.50"]
18
+
19
+ [project.scripts]
20
+ bwssh = "bwssh.cli:main"
21
+ bwssh-agentd = "bwssh.daemon:main_entry"
22
+
23
+ [dependency-groups]
24
+ dev = [
25
+ "ruff>=0.9",
26
+ "pytest>=8.0",
27
+ "pytest-cov>=6.0",
28
+ "mypy>=1.14",
29
+ "pytest-asyncio>=1.3.0",
30
+ ]
31
+
32
+ [build-system]
33
+ requires = ["uv_build>=0.9.28,<0.10.0"]
34
+ build-backend = "uv_build"
35
+
36
+ [tool.ruff]
37
+ target-version = "py312"
38
+ line-length = 88
39
+ src = ["src", "tests"]
40
+
41
+ [tool.ruff.lint]
42
+ select = [
43
+ "E", # pycodestyle errors
44
+ "W", # pycodestyle warnings
45
+ "F", # pyflakes
46
+ "I", # isort
47
+ "B", # flake8-bugbear
48
+ "C4", # flake8-comprehensions
49
+ "UP", # pyupgrade
50
+ "ARG", # flake8-unused-arguments
51
+ "SIM", # flake8-simplify
52
+ "TCH", # flake8-type-checking
53
+ "PTH", # flake8-use-pathlib
54
+ "ERA", # eradicate (commented out code)
55
+ "PL", # pylint
56
+ "RUF", # ruff-specific rules
57
+ ]
58
+ ignore = [
59
+ "PLR0913", # too many arguments
60
+ "PLR2004", # magic value comparison
61
+ ]
62
+
63
+ [tool.ruff.lint.isort]
64
+ known-first-party = ["bwssh"]
65
+
66
+ [tool.ruff.format]
67
+ quote-style = "double"
68
+ indent-style = "space"
69
+ skip-magic-trailing-comma = false
70
+ line-ending = "auto"
71
+
72
+ [tool.mypy]
73
+ python_version = "3.12"
74
+ strict = true
75
+ warn_return_any = true
76
+ warn_unused_ignores = true
77
+ disallow_untyped_defs = true
78
+ disallow_incomplete_defs = true
79
+
80
+ [[tool.mypy.overrides]]
81
+ module = "textual.*"
82
+ ignore_missing_imports = true
83
+
84
+ [[tool.mypy.overrides]]
85
+ module = "gi.*"
86
+ ignore_missing_imports = true
87
+
88
+ [tool.pytest.ini_options]
89
+ testpaths = ["tests"]
90
+ pythonpath = ["src"]
91
+ addopts = ["-ra", "-q", "--strict-markers"]
92
+
93
+ [tool.coverage.run]
94
+ source = ["src/bwssh"]
95
+ branch = true
96
+
97
+ [tool.coverage.report]
98
+ exclude_lines = [
99
+ "pragma: no cover",
100
+ "if TYPE_CHECKING:",
101
+ "if __name__ == .__main__.:",
102
+ ]
@@ -0,0 +1,7 @@
1
+ """bwssh - Add your description here."""
2
+
3
+ import logging
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ logger = logging.getLogger(__name__)
@@ -0,0 +1,66 @@
1
+ """SSH agent protocol message framing over asyncio streams.
2
+
3
+ Wire format (per IETF draft-miller-ssh-agent-17 §3):
4
+ [uint32 length][byte msg_type][payload...]
5
+
6
+ Length field = 1 (type byte) + len(payload). All multi-byte integers are big-endian.
7
+
8
+ SSH string format:
9
+ [uint32 length][data...]
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import struct
16
+ from typing import TYPE_CHECKING
17
+
18
+ if TYPE_CHECKING:
19
+ import asyncio
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ _UINT32 = struct.Struct(">I")
24
+
25
+
26
+ def pack_uint32(n: int) -> bytes:
27
+ return _UINT32.pack(n)
28
+
29
+
30
+ def unpack_uint32(data: bytes, offset: int) -> tuple[int, int]:
31
+ (value,) = _UINT32.unpack_from(data, offset)
32
+ return value, offset + 4
33
+
34
+
35
+ def pack_string(data: bytes) -> bytes:
36
+ return _UINT32.pack(len(data)) + data
37
+
38
+
39
+ def unpack_string(data: bytes, offset: int) -> tuple[bytes, int]:
40
+ length, offset = unpack_uint32(data, offset)
41
+ end = offset + length
42
+ if end > len(data):
43
+ raise ValueError(
44
+ f"SSH string at offset {offset - 4}: need {length} bytes, "
45
+ f"have {len(data) - offset}"
46
+ )
47
+ return data[offset:end], end
48
+
49
+
50
+ async def read_message(reader: asyncio.StreamReader) -> tuple[int, bytes]:
51
+ raw_length = await reader.readexactly(4)
52
+ (length,) = _UINT32.unpack(raw_length)
53
+
54
+ raw_body = await reader.readexactly(length)
55
+ msg_type = raw_body[0]
56
+ payload = raw_body[1:]
57
+ return msg_type, payload
58
+
59
+
60
+ async def write_message(
61
+ writer: asyncio.StreamWriter, msg_type: int, payload: bytes
62
+ ) -> None:
63
+ length = 1 + len(payload)
64
+ header = _UINT32.pack(length) + bytes([msg_type])
65
+ writer.write(header + payload)
66
+ await writer.drain()