Compare commits

..

1 commit

Author SHA1 Message Date
Joao Jacome 320182effc Fixed path 2020-04-07 13:25:18 +01:00
14 changed files with 101 additions and 522 deletions

View file

@ -1,25 +0,0 @@
name: black
on:
push:
branches:
- master
pull_request: {}
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install black
run: |
pip install --upgrade pip
python3.10 -m venv env
source env/bin/activate
pip install black
- name: Run black
run: |
env/bin/black --check --verbose bw_add_sshkeys.py

View file

@ -1,25 +0,0 @@
name: flake8
on:
push:
branches:
- master
pull_request: {}
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install flake8
run: |
pip install --upgrade pip
python3.10 -m venv env
source env/bin/activate
pip install flake8
- name: Run black
run: |
env/bin/flake8 bw_add_sshkeys.py

View file

@ -1,25 +0,0 @@
name: mypy
on:
push:
branches:
- master
pull_request: {}
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install mypy
run: |
pip install --upgrade pip
python3.10 -m venv env
source env/bin/activate
pip install mypy types-setuptools
- name: Run black
run: |
env/bin/mypy bw_add_sshkeys.py

3
.gitignore vendored
View file

@ -1,3 +0,0 @@
.pyenv/
.mypy_cache/
env/

21
LICENSE
View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2020-2021 Joao Jacome
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,41 +1,31 @@
# Bitwarden SSH Agent # Bitwarden SSH Agent
## Requirements ## Requirements
* You need to have the [Bitwarden CLI tool](https://bitwarden.com/help/cli/) installed and available in the `$PATH` as `bw`. See below for detailed instructions.
* `ssh-agent` must be running in the current session.
## Installation * You need to have the bitwarden cli `bw` installed
Just save the file `bw_add_sshkeys.py` in a folder where it can by found when calling it from the command line. On linux you can see these folders by running `echo $PATH` from the command line. To install for a single user, you can - for example - save the script under `~/.local/bin/` and make it executable by running `chmod +x ~/.local/bin/bw_add_sshkeys.py`. * ssh-agent must be running in the current session
## What does it do? ## What it does?
Fetches SSH keys stored in Bitwarden vault and adds them to `ssh-agent`.
* connects to bitwarden using the bitwarden cli
* looks for a folder called `ssh-agent`
* loads the ssh key for each item in that folder
## How to use it ## How to use it
1. Run,
```shell `$ ./start.sh`
./bw_add_sshkeys.py
``` Fill in you login information
2. Enter your Bitwarden credentials, if a Bitwarden vault session is not already set.
3. (optional) Enter your SSH keys' passphrases if they're not stored in your Bitwarden.
## Storing the keys in BitWarden ## Storing the keys in BitWarden
1. Create a folder called `ssh-agent` (can be overridden on the command line).
2. Add an new secure note to that folder.
3. Upload the private key as an attachment.
4. Add the custom field `private` (can be overridden on the command line), containing the file name of the private key attachment.
5. (optional) If your key is encrypted with passphrase and you want it to decrypt automatically, save passphrase into custom field `passphrase` (field name can be overriden on the command line). You can create this field as `hidden` if you don't want the passphrase be displayed by default.
6. Repeat steps 2-5 for each subsequent key
## Command line overrides 1. Create a folder called 'ssh-agent'
* `--debug`/`-d` - Show debug output 2. Add an new secure note to that folder
* `--foldername`/`-f` - Folder name to use to search for SSH keys _(default: ssh-agent)_ 3. Upload the private_key as an attachment
* `--customfield`/`-c` - Custom field name where private key filename is stored _(default: private)_ 4. add the custom field `private`, containing the private key filename
* `--passphrasefield`/`-p` - Custom field name where passphrase for the key is stored _(default: passphrase)_
* `--session`/`-s` - session key of bitwarden
## Setting up the Bitwarden CLI tool
Download the [Bitwarden CLI](https://bitwarden.com/help/cli/), extract the binary from the zip file, make it executable and add it to your path so that it can be found on the command line.
On linux you will likely want to move the executable to `~/.local/bin` and make it executable `chmod +x ~/.local/bin/bw`. `~/.local/bin` is likely already set as a path. You can confirm that by running `which bw`, which should return the path to the executable. You can use the same approach to turn `bw_add_sshkeys.py` into an executable. ## Improvements to be made
If you want to build the Bitwarden CLI by yourself, see [these instructions on the bitwarden github page](https://contributing.bitwarden.com/getting-started/clients/cli). * Find a way to extract the attachment from bitwarden in memory, instead of creating a file for it

View file

@ -1,277 +0,0 @@
#!/usr/bin/env python3
"""
Extracts SSH keys from Bitwarden vault
"""
import argparse
import json
import logging
import os
import subprocess
from typing import Any
def get_session(session: str) -> str:
"""
Function to return a valid Bitwarden session
"""
# Check for an existing, user-supplied Bitwarden session
if not session:
session = os.environ.get("BW_SESSION", "")
if session:
logging.debug("Existing Bitwarden session found")
return session
# Check if we're already logged in
proc_logged = subprocess.run(["bw", "login", "--check", "--quiet"], check=False)
if proc_logged.returncode:
logging.debug("Not logged into Bitwarden")
operation = "login"
else:
logging.debug("Bitwarden vault is locked")
operation = "unlock"
proc_session = subprocess.run(
["bw", "--raw", operation],
stdout=subprocess.PIPE,
universal_newlines=True,
check=True,
)
session = proc_session.stdout
logging.info(
'To re-use this BitWarden session run: export BW_SESSION="%s"',
session,
)
return session
def get_folders(session: str, foldername: str) -> str:
"""
Function to return the ID of the folder that matches the provided name
"""
logging.debug("Folder name: %s", foldername)
proc_folders = subprocess.run(
["bw", "list", "folders", "--search", foldername, "--session", session],
stdout=subprocess.PIPE,
universal_newlines=True,
check=True,
encoding="utf-8",
)
folders = json.loads(proc_folders.stdout)
if not folders:
logging.error('"%s" folder not found', foldername)
return ""
# Do we have any folders
if len(folders) != 1:
logging.error('%d folders with the name "%s" found', len(folders), foldername)
return ""
return str(folders[0]["id"])
def folder_items(session: str, folder_id: str) -> list[dict[str, Any]]:
"""
Function to return items from a folder
"""
logging.debug("Folder ID: %s", folder_id)
proc_items = subprocess.run(
["bw", "list", "items", "--folderid", folder_id, "--session", session],
stdout=subprocess.PIPE,
universal_newlines=True,
check=True,
encoding="utf-8",
)
data: list[dict[str, Any]] = json.loads(proc_items.stdout)
return data
def add_ssh_keys(
session: str,
items: list[dict[str, Any]],
keyname: str,
pwkeyname: str,
) -> None:
"""
Function to attempt to get keys from a vault item
"""
for item in items:
try:
private_key_file = [
k["value"] for k in item["fields"] if k["name"] == keyname
][0]
except IndexError:
logging.warning('No "%s" field found for item %s', keyname, item["name"])
continue
except KeyError as error:
logging.debug(
'No key "%s" found in item %s - skipping', error.args[0], item["name"]
)
continue
logging.debug("Private key file declared")
private_key_pw = ""
try:
private_key_pw = [
k["value"] for k in item["fields"] if k["name"] == pwkeyname
][0]
logging.debug("Passphrase declared")
except IndexError:
logging.warning('No "%s" field found for item %s', pwkeyname, item["name"])
except KeyError as error:
logging.debug(
'No key "%s" found in item %s - skipping', error.args[0], item["name"]
)
try:
private_key_id = [
k["id"]
for k in item["attachments"]
if k["fileName"] == private_key_file
][0]
except IndexError:
logging.warning(
'No attachment called "%s" found for item %s',
private_key_file,
item["name"],
)
continue
logging.debug("Private key ID found")
try:
ssh_add(session, item["id"], private_key_id, private_key_pw)
except subprocess.SubprocessError:
logging.warning("Could not add key to the SSH agent")
def ssh_add(session: str, item_id: str, key_id: str, key_pw: str = "") -> None:
"""
Function to get the key contents from the Bitwarden vault
"""
logging.debug("Item ID: %s", item_id)
logging.debug("Key ID: %s", key_id)
proc_attachment = subprocess.run(
[
"bw",
"get",
"attachment",
key_id,
"--itemid",
item_id,
"--raw",
"--session",
session,
],
stdout=subprocess.PIPE,
universal_newlines=True,
check=True,
)
ssh_key = proc_attachment.stdout
if key_pw:
envdict = dict(
os.environ,
SSH_ASKPASS=os.path.realpath(__file__),
SSH_KEY_PASSPHRASE=key_pw,
)
else:
envdict = dict(os.environ, SSH_ASKPASS_REQUIRE="never")
logging.debug("Running ssh-add")
# CAVEAT: `ssh-add` provides no useful output, even with maximum verbosity
subprocess.run(
["ssh-add", "-"],
input=ssh_key.encode("utf-8"),
# Works even if ssh-askpass is not installed
env=envdict,
universal_newlines=False,
check=True,
)
if __name__ == "__main__":
def parse_args() -> argparse.Namespace:
"""
Function to parse command line arguments
"""
parser = argparse.ArgumentParser()
parser.add_argument(
"-d",
"--debug",
action="store_true",
help="show debug output",
)
parser.add_argument(
"-f",
"--foldername",
default="ssh-agent",
help="folder name to use to search for SSH keys",
)
parser.add_argument(
"-c",
"--customfield",
default="private",
help="custom field name where private key filename is stored",
)
parser.add_argument(
"-p",
"--passphrasefield",
default="passphrase",
help="custom field name where key passphrase is stored",
)
parser.add_argument(
"-s",
"--session",
default="",
help="session key of bitwarden",
)
return parser.parse_args()
def main() -> None:
"""
Main program logic
"""
args = parse_args()
if args.debug:
loglevel = logging.DEBUG
else:
loglevel = logging.INFO
logging.basicConfig(level=loglevel)
try:
logging.info("Getting Bitwarden session")
session = get_session(args.session)
logging.debug("Session = %s", session)
logging.info("Getting folder list")
folder_id = get_folders(session, args.foldername)
logging.info("Getting folder items")
items = folder_items(session, folder_id)
logging.info("Attempting to add keys to ssh-agent")
add_ssh_keys(session, items, args.customfield, args.passphrasefield)
except subprocess.CalledProcessError as error:
if error.stderr:
logging.error('"%s" error: %s', error.cmd[0], error.stderr)
logging.debug("Error running %s", error.cmd)
if os.environ.get("SSH_ASKPASS") and os.environ.get(
"SSH_ASKPASS"
) == os.path.realpath(__file__):
print(os.environ.get("SSH_KEY_PASSPHRASE"))
else:
main()

View file

@ -1,61 +0,0 @@
{
"nodes": {
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1690933134,
"narHash": "sha256-ab989mN63fQZBFrkk4Q8bYxQCktuHmBIBqUG1jl6/FQ=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "59cf3f1447cfc75087e7273b04b31e689a8599fb",
"type": "github"
},
"original": {
"id": "flake-parts",
"type": "indirect"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1691464053,
"narHash": "sha256-D21ctOBjr2Y3vOFRXKRoFr6uNBvE8q5jC4RrMxRZXTM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "844ffa82bbe2a2779c86ab3a72ff1b4176cec467",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1690881714,
"narHash": "sha256-h/nXluEqdiQHs1oSgkOOWF+j8gcJMWhwnZ9PFabN6q0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9e1960bc196baf6881340d53dccb203a951745a2",
"type": "github"
},
"original": {
"dir": "lib",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View file

@ -1,44 +0,0 @@
{
description = "Small python script to load bitwarden-store ssh keys into ssh-agent";
outputs = inputs@{ nixpkgs, flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [
inputs.flake-parts.flakeModules.easyOverlay
];
systems = [ "x86_64-linux" "aarch64-darwin" ];
perSystem = { config, self', inputs', pkgs, system, ... }:
let
package = pkgs.python3Packages.buildPythonPackage {
pname = "bitwarden-ssh-agent";
version = "0.1.2";
src = ./. ;
propagatedBuildInputs = [ pkgs.python3Packages.setuptools pkgs.bitwarden-cli ];
format = "other";
installPhase = ''
mkdir -p $out/bin/
mv bw_add_sshkeys.py $out/bin/bitwarden-ssh-agent
chmod +x $out/bin/bitwarden-ssh-agent
'';
meta = with pkgs.lib; {
description = "Small python script to load bitwarden-store ssh keys into ssh-agent";
homepage = "https://github.com/joaojacome/bitwarden-ssh-agent";
license = licenses.mit;
};
};
in {
overlayAttrs = {
bitwarden-ssh-agent = package;
};
packages.default = package;
devShells.default = pkgs.mkShell {
packages = [
pkgs.bitwarden-cli
(pkgs.python3.withPackages(pkgs: [
pkgs.setuptools
]))
];
};
};
};
}

View file

@ -1,9 +0,0 @@
[mypy]
python_version = 3.10
warn_return_any = True
disallow_untyped_defs = True
disallow_any_unimported = True
no_implicit_optional = True
check_untyped_defs = True
show_error_codes = True
warn_unused_ignores = True

View file

@ -1,2 +0,0 @@
[tool.black]
target-version = ['py310']

View file

@ -1,2 +0,0 @@
[flake8]
max-line-length = 100

68
ssh.py Executable file
View file

@ -0,0 +1,68 @@
import subprocess
import os
import sys
import json
import tempfile
from urllib.parse import urlparse, urlencode
from urllib.request import urlopen, Request
from urllib.error import HTTPError
try:
subprocess.check_output(['bw', 'logout'])
except:
pass
try:
session = subprocess.check_output(['bw', '--raw', 'login'])
session = ['--session', session]
except:
print('Couldnt login!')
sys.exit(1)
try:
folders = subprocess.check_output(['bw','list', 'folders', '--search', 'ssh-agent'] + session)
folders = json.loads(folders)
if not folders:
raise AttributeError
if len(folders) != 1:
raise ValueError
except AttributeError:
print('Couldnt find ssh-agent folder!')
sys.exit(1)
except ValueError:
print('More than one ssh-agent folder found!')
sys.exit(1)
except:
print('Error retrieving folders.')
sys.exit(1)
folder = folders[0]['id']
try:
items = subprocess.check_output(['bw', 'list', 'items', '--folderid', folder, 'ssh-agent'] + session)
items = json.loads(items)
except Exception as e:
print('Cant fint items.')
print(e)
sys.exit(1)
keys = []
directory = tempfile.TemporaryDirectory()
temporary_file_name = os.path.join(directory.name + './private_key')
try:
for item in items:
private_key_file = [k['value'] for k in item['fields'] if k['name'] == 'private' and k['type'] == 0][0]
private_key_id = [k['id'] for k in item['attachments'] if k['fileName'] == private_key_file][0]
# would be nice if there was an option to retrieve the attachment file directly to the stdout
subprocess.check_output(['bw', 'get', 'attachment', private_key_id, '--itemid', item['id'], '--output', temporary_file_name] + session)
private_key = open(temporary_file_name, 'r').read()
os.remove(temporary_file_name)
keys.append({'private_key': private_key})
except:
print('Something happened.')
sys.exit(1)
print(';'.join([k['private_key'] for k in keys]))

15
start.sh Executable file
View file

@ -0,0 +1,15 @@
#!/bin/bash
(
SCRIPTPATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
cd $SCRIPTPATH
KEYS=$(/usr/bin/env python ssh.py)
IFS=';'
read -d '' -ra SPLITKEYS < <(printf '%s;\0' "$KEYS")
for i in ${SPLITKEYS[@]}
do
ssh-add - <<< "${i}"
done
)