1
0
mirror of https://github.com/ssh-vault/ssh-vault.git synced 2025-04-19 07:42:18 +03:00
This commit is contained in:
nbari 2023-04-24 20:58:43 +02:00
parent 4420c04017
commit 5c4ef780b5
No known key found for this signature in database
78 changed files with 4946 additions and 2127 deletions

2
.codespellrc Normal file
View File

@ -0,0 +1,2 @@
[codespell]
ignore-words-list = crate

5
.devcontainer/Dockerfile Normal file
View File

@ -0,0 +1,5 @@
FROM rust
RUN apt update && apt install -y --no-install-recommends -q build-essential ca-certificates curl git gnupg2 jq netcat openssl sudo vim zsh
RUN rustup component add rustfmt clippy
RUN cargo install cargo-expand cargo-edit
WORKDIR /home/

View File

@ -0,0 +1,19 @@
{
"name": "Rust",
"extensions": [
"cschleiden.vscode-github-actions",
"ms-vsliveshare.vsliveshare",
"matklad.rust-analyzer",
"serayuzgur.crates",
"vadimcn.vscode-lldb"
],
"dockerFile": "Dockerfile",
"settings": {
"editor.formatOnSave": true,
"terminal.integrated.shell.linux": "/usr/bin/zsh",
"files.exclude": {
"**/CODE_OF_CONDUCT.md": true,
"**/LICENSE": true
}
}
}

View File

@ -1,38 +0,0 @@
name: build
on:
release:
types:
- created
jobs:
releases-matrix:
name: Release Go Binary
runs-on: ubuntu-latest
strategy:
matrix:
goos: [freebsd, linux, darwin]
goarch: ["386", amd64, arm64]
exclude:
- goarch: "386"
goos: darwin
steps:
- uses: actions/checkout@v2
- name: set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Test
run: |
echo $RELEASE_VERSION
echo ${{ env.RELEASE_VERSION }}
#- name: get version
#run: echo "GIT_TAG=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV
- uses: actions/checkout@v2
- uses: wangyoucao577/go-release-action@v1.30
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
project_path: ./cmd/ssh-vault
binary_name: ssh-vault
ldflags: -s -w -X main.version=${{ env.RELEASE_VERSION }}

View File

@ -1,71 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master, develop ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '32 20 * * 2'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@ -1,3 +1,4 @@
---
on:
release:
types:
@ -9,7 +10,8 @@ jobs:
name: Bump Homebrew formula
runs-on: ubuntu-latest
steps:
- uses: mislav/bump-homebrew-formula-action@v2.1
- name: bump-homebrew-formula
uses: mislav/bump-homebrew-formula-action@v3.1
with:
# A PR will be sent to github.com/Homebrew/homebrew-core to update this formula:
formula-name: ssh-vault

View File

@ -1,20 +1,54 @@
---
name: test
on: [push, pull_request]
on:
- push
- pull_request
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Setup | Checkout
uses: actions/checkout@v2
- name: Setup | Rust
uses: ATiltedTree/setup-rust@v1
with:
rust-version: stable
components: clippy
- name: Build | Lint
run: cargo clippy -- -D clippy::all -D clippy::nursery -D warnings
compile:
name: Compile
runs-on: ubuntu-latest
steps:
- name: Setup | Checkout
uses: actions/checkout@v2
- name: Setup | Rust
uses: ATiltedTree/setup-rust@v1
with:
rust-version: stable
- name: Build | Compile
run: cargo check
test:
name: Test
strategy:
matrix:
go-version: [1.18.x, 1.19.x]
os: [ubuntu-latest, macos-latest]
os:
- ubuntu-latest
- macOS-latest
rust:
- stable
runs-on: ${{ matrix.os }}
needs:
- compile
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Test
run: go test ./...
- name: Setup | Checkout
uses: actions/checkout@v2
- name: Setup | Rust
uses: ATiltedTree/setup-rust@v1
with:
rust-version: ${{ matrix.rust }}
- name: Build | Compile
run: cargo test

10
.gitignore vendored
View File

@ -1,7 +1,3 @@
ssh-vault
!*ssh-vault/
coverage.*
.goxc.local.json
.goxc.json
build
vendor
vault.gpg
/target/
**/*.rs.bk

View File

@ -1,3 +1,8 @@
## 1.0.0
* Support for ed25519 keys
* Legacy keys header (`-----BEGIN RSA PRIVATE KEY-----`) need to be updated using `ssh-keygen -p`
* moving to rust 🦀
## 0.12.8
* Support encrypted openssh private keys [#50](https://github.com/ssh-vault/ssh-vault/pull/50)

2354
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

38
Cargo.toml Normal file
View File

@ -0,0 +1,38 @@
[package]
name = "ssh-vault"
version = "1.0.0"
authors = ["Nicolas Embriz <nbari@tequila.io>"]
description = "encrypt/decrypt using ssh keys"
documentation = "https://ssh-vault.com/"
repository = "https://github.com/ssh-vault/ssh-vault"
homepage = "https://ssh-vault.com/"
readme = "README.md"
keywords = ["ssh", "encryption", "fingerprint"]
categories = ["command-line-utilities", "cryptography"]
license = "BSD-3-Clause"
edition = "2021"
[dependencies]
aes-gcm = "0.10.3"
anyhow = "1"
atty = "0.2.14"
base58 = "0.2.0"
base64ct = { version = "1.6.0", features = ["alloc"] }
chacha20poly1305 = "0.10.1"
clap = { version = "4.4", features = ["env"] }
ed25519-dalek = { version = "2.0.0", features = ["pkcs8"] }
file_shred = "1.1.3"
hex-literal = "0.4.1"
hkdf = "0.12.3"
home = "0.5.5"
md5 = "0.7.0"
rand = "0.8.5"
reqwest = { version = "0.11", features = ["blocking"] }
rpassword = "7.3"
rsa = { version = "0.9.3", features = ["sha2"] }
secrecy = "0.8.0"
sha2 = "0.10.8"
ssh-key = { version = "0.6.2", features = ["ed25519", "rsa", "encryption"] }
tempfile = "3.8.0"
url = "2.4.1"
x25519-dalek = { version = "2.0.0", features = ["getrandom", "static_secrets"] }

View File

@ -2,8 +2,6 @@
[![build](https://github.com/ssh-vault/ssh-vault/actions/workflows/build.yml/badge.svg)](https://github.com/ssh-vault/ssh-vault/actions/workflows/build.yml)
[![test](https://github.com/ssh-vault/ssh-vault/actions/workflows/test.yml/badge.svg)](https://github.com/ssh-vault/ssh-vault/actions/workflows/test.yml)
[![Coverage Status](https://coveralls.io/repos/github/ssh-vault/ssh-vault/badge.svg?branch=develop)](https://coveralls.io/github/ssh-vault/ssh-vault?branch=develop)
[![Go Report Card](https://goreportcard.com/badge/github.com/ssh-vault/ssh-vault)](https://goreportcard.com/report/github.com/ssh-vault/ssh-vault)
encrypt/decrypt using ssh private keys
@ -15,9 +13,46 @@ https://ssh-vault.com
$ ssh-vault -h
Example:
$ echo "secret" | ssh-vault -u <github.com/user> create
```txt
encrypt/decrypt using ssh keys
Usage: ssh-vault [COMMAND]
Commands:
create Create a new vault [aliases: c]
edit Edit an existing vault [aliases: e]
fingerprint Print the fingerprint of a public ssh key [aliases: f]
view View an existing vault [aliases: v]
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version
```
Examples:
Create a vault:
```sh
$ echo "secret" | ssh-vault create -u <github.com/user>
```
View a vault:
```sh
echo "SSH-VAULT..."| ssh-vault view
```
Share a secret:
```sh
$ echo "secret" | ssh-vault create -u new
```
## Installation
@ -25,34 +60,9 @@ Example:
### Mac OS
brew install ssh-vault
### Binaries
Binaries and packages for a variety of platforms are published to Bintray:
[ ![Download](https://api.bintray.com/packages/nbari/ssh-vault/ssh-vault/images/download.svg) ](https://dl.bintray.com/nbari/ssh-vault/)
### Using Cargo
To download a specific version, use URL like https://dl.bintray.com/nbari/ssh-vault/ssh-vault_0.12.4_amd64.deb
To download the latest version:
PACKAGING=amd64.deb
LATEST_VERSION=$(curl -w "%{redirect_url}" -o /dev/null -s https://bintray.com/nbari/ssh-vault/ssh-vault/_latestVersion | sed 's|.*/||')
curl -L -O "https://dl.bintray.com/nbari/ssh-vault/ssh-vault_${LATEST_VERSION}_${PACKAGING}"
### Compile from source
Setup go environment https://golang.org/doc/install
For example, using $HOME/go for your workspace
$ export GOPATH=$HOME/go
Get the code:
$ go get github.com/ssh-vault/ssh-vault
Build by just typing make:
$ cd $GOPATH/src/github.com/ssh-vault/ssh-vault
$ make
$ cargo install ssh-vault
## Issues

View File

@ -1,9 +0,0 @@
FROM golang:latest as builder
RUN go get -u github.com/golang/dep/cmd/dep
WORKDIR /go/src/github.com/ssh-vault/ssh-vault
copy . .
ARG VERSION=0.0.0
ENV VERSION="${VERSION}"
RUN dep ensure --vendor-only
RUN make test
RUN make build-linux

View File

@ -1,68 +0,0 @@
.PHONY: all get test clean build cover compile goxc bintray
GO ?= go
BIN_NAME=ssh-vault
GO_XC = ${GOPATH}/bin/goxc -os="freebsd netbsd openbsd darwin linux windows" -bc="!386"
GOXC_FILE = .goxc.json
GOXC_FILE_LOCAL = .goxc.local.json
VERSION=$(shell git describe --tags --always)
all: clean build
get:
${GO} mod tidy
build: get
${GO} build -ldflags "-s -w -X main.version=${VERSION}" -o ${BIN_NAME} cmd/ssh-vault/main.go;
build-linux:
for arch in amd64 arm64; do \
mkdir -p build/$${arch}; \
GOOS=linux GOARCH=$${arch} ${GO} build -ldflags "-s -w -X main.version=${VERSION}" -o build/$${arch}/${BIN_NAME} cmd/ssh-vault/main.go; \
done
clean:
@rm -rf ssh-vault-* ${BIN_NAME} ${BIN_NAME}.debug *.out build debian
test: get
${GO} test -race -v
cover:
${GO} test -cover && \
${GO} test -coverprofile=coverage.out && \
${GO} tool cover -html=coverage.out
compile: clean goxc
goxc:
$(shell echo '{\n "ConfigVersion": "0.9",' > $(GOXC_FILE))
$(shell echo ' "AppName": "ssh-vault",' >> $(GOXC_FILE))
$(shell echo ' "ArtifactsDest": "build",' >> $(GOXC_FILE))
$(shell echo ' "PackageVersion": "${VERSION}",' >> $(GOXC_FILE))
$(shell echo ' "TaskSettings": {' >> $(GOXC_FILE))
$(shell echo ' "bintray": {' >> $(GOXC_FILE))
$(shell echo ' "downloadspage": "bintray.md",' >> $(GOXC_FILE))
$(shell echo ' "package": "ssh-vault",' >> $(GOXC_FILE))
$(shell echo ' "repository": "ssh-vault",' >> $(GOXC_FILE))
$(shell echo ' "subject": "nbari"' >> $(GOXC_FILE))
$(shell echo ' }\n },' >> $(GOXC_FILE))
$(shell echo ' "BuildSettings": {' >> $(GOXC_FILE))
$(shell echo ' "LdFlags": "-s -w -X main.version=${VERSION}"' >> $(GOXC_FILE))
$(shell echo ' }\n}' >> $(GOXC_FILE))
$(shell echo '{\n "ConfigVersion": "0.9",' > $(GOXC_FILE_LOCAL))
$(shell echo ' "TaskSettings": {' >> $(GOXC_FILE_LOCAL))
$(shell echo ' "bintray": {\n "apikey": "$(BINTRAY_APIKEY)"' >> $(GOXC_FILE_LOCAL))
$(shell echo ' }\n } \n}' >> $(GOXC_FILE_LOCAL))
${GO_XC}
bintray:
${GO_XC} bintray
docker:
docker build -t ssh-vault --build-arg VERSION=${VERSION} .
docker-no-cache:
docker build --no-cache -t ssh-vault --build-arg VERSION=${VERSION} .
linux:
docker run --entrypoint "/bin/bash" -it --privileged ssh-vault

View File

@ -1,24 +0,0 @@
package sshvault
import (
"os"
"reflect"
"runtime"
"testing"
"time"
)
/* Test Helpers */
func expect(t *testing.T, a interface{}, b interface{}) {
_, fn, line, _ := runtime.Caller(1)
if a != b {
t.Errorf("Expected: %v (type %v) Got: %v (type %v) in %s:%d", a, reflect.TypeOf(a), b, reflect.TypeOf(b), fn, line)
}
}
// PtyWriteback
func PtyWriteback(pty *os.File, msg string) {
time.Sleep(500 * time.Millisecond)
defer pty.Sync()
pty.Write([]byte(msg))
}

View File

@ -1,130 +0,0 @@
package sshvault
import (
"crypto/md5"
"encoding/pem"
"fmt"
"io/ioutil"
"log"
"os"
"os/user"
"path/filepath"
"strings"
"github.com/ssh-vault/ssh2pem"
)
type cache struct {
dir string
}
// Cache creates ~/.ssh/vault
func Cache() *cache {
d := os.Getenv("SSH_VAULT_CACHE_DIR")
if d == "" {
home := os.Getenv("HOME")
if home == "" {
usr, _ := user.Current()
home = usr.HomeDir
}
d = filepath.Join(home, ".ssh", "vault", "keys")
}
if _, err := os.Stat(d); os.IsNotExist(err) {
if err := os.MkdirAll(d, os.ModePerm); err != nil {
log.Fatal(err)
}
}
return &cache{d}
}
// Get return ssh-key
func (c *cache) Get(s Schlosser, u, f string, k int) (string, error) {
if k == 0 {
k = 1
}
// storage format
// ~/.ssh/vault/keys/user.key-N
// or
// ~/.ssh/vault/keys/<md5>.key-N
var (
uKey string
hash string
)
if !isURL.MatchString(u) {
uKey = fmt.Sprintf("%s/%s.%d", c.dir, u, k)
} else {
hash = fmt.Sprintf("%x", md5.Sum([]byte(u)))
uKey = fmt.Sprintf("%s/%s.%d", c.dir, hash, k)
}
// if key not found, fetch it
if !c.IsFile(uKey) || u == "new" {
keys, err := s.GetKey(u)
if err != nil {
return "", err
}
if isURL.MatchString(u) {
u = hash
}
for k, v := range keys {
err = ioutil.WriteFile(fmt.Sprintf("%s/%s.%d", c.dir, u, k+1),
[]byte(v),
0644)
if err != nil {
log.Println(err)
}
}
if !c.IsFile(uKey) {
return "", fmt.Errorf("key index not found, try -k with a value between 1 and %d", len(keys))
}
}
// if fingerprint, find the key that matches
if f != "" {
key, err := c.FindFingerprint(uKey, f)
if err != nil {
return "", err
}
return key, nil
}
return uKey, nil
}
// IsFile check if string is a file
func (c *cache) IsFile(path string) bool {
f, err := os.Stat(path)
if err != nil {
return false
}
if m := f.Mode(); !m.IsDir() && m.IsRegular() && m&400 != 0 {
return true
}
return false
}
// Find searches for key
func (c *cache) FindFingerprint(u, f string) (string, error) {
files, err := ioutil.ReadDir(c.dir)
if err != nil {
return "", err
}
var user = strings.TrimSuffix(filepath.Base(u), filepath.Ext(filepath.Base(u)))
for _, file := range files {
if strings.HasPrefix(file.Name(), user) {
out, err := ssh2pem.GetPem(filepath.Join(c.dir, file.Name()))
if err != nil {
return "", err
}
p, _ := pem.Decode(out)
x := &vault{}
fingerprint, _ := x.GenFingerprint(p)
if f == fingerprint {
return filepath.Join(c.dir, file.Name()), nil
}
}
}
return "", fmt.Errorf("key fingerprint: %q not found", f)
}

View File

@ -1,90 +0,0 @@
package sshvault
import (
"fmt"
"io/ioutil"
"os"
"strings"
"testing"
)
func TestCacheIsFile(t *testing.T) {
cache := &cache{}
if cache.IsFile("/") {
t.Errorf("Expecting false")
}
if !cache.IsFile("cache_test.go") {
t.Errorf("Expecting true")
}
}
type mockSchlosser struct{}
func (m mockSchlosser) GetKey(u string) ([]string, error) {
switch u {
case "alice":
return []string{"ssh-rsa ABC"}, nil
case "bob":
return nil, nil
case "matilde":
return []string{"ssh-rsa ABC", "ssh-rsa ABC", "ssh-rsa ABC"}, nil
case "pedro":
return []string{"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCrrjZ4Hw9wj/RXaNmwAS0eAxub9LYYCv4bsfxE4UmXcLQSj4YIM8+GfsPkykKZNl5+iatzeKrolYCHLIjC1xwsC199o5lpEBskV1g0uFhRiuguUJxM2r66bbxOfuSZcY9tHD/NkgLg0rTqDzGXtkWbBbjtam9N0H4dbCfgVpGVI8feZqFR5uiukG2eDJKn+0S4UTwZgO7TvSxpMl31xqlPy9EsgEhb+19YYuvSQOXWBX6yuKr1gjY7g3/wmtXRdrZbTjZmIeACITNWgWM7TFEqYf88bHHAMz1pSj5V8Uu0k/yEd2RRIHoMc1fMq5ygMEU6mcEf3C8zy6w5r3rRms2n", "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGOhBrPToSBJCblZoK44w3/ub3K6Vx39ilHB/2sJIDqLZTx8I1U2l2RD3WhwKXdqqpH6RZh0piGlWuGV/E7xOseH9qEOKZMgscdvNO9nzD8jkSlShhZQUmhWOqLPcVUDlgIubxrFRVODcFxqgJwjm+qR2X2GaHJottrn5jFhNBEYcjdnuDKXZQ7Cr+K2bOcD+pvhMI7/qtR7jKa7Q5BoRxQEsNQEZvvgJpen2CqAsnjpJXjAXttnXJnAXcyYyOe8ZOCY/tkmXWvn9Fkd1EYmK14rB8WNEe+vraNCS9tSi1PyLMJWr3XNeluLr2/y7gHSyO6xzQNoXiTDDBFW2y3VK5", "ssh-rsa AAA", "ssh-rsa BBB"}, nil
default:
return nil, fmt.Errorf("Error")
}
}
func TestCacheGet(t *testing.T) {
dir, err := ioutil.TempDir("", "cache")
if err != nil {
t.Error(err)
}
defer os.RemoveAll(dir) // clean up
var testTable = []struct {
user string
key int
out string
err bool
}{
{"alice", 0, "alice.key-1", false},
{"alice", 1, "alice.key-1", false},
{"alice", 2, "", true},
{"bob", 1, "", true},
{"matilde", 3, "matilde.key-3", false},
{"matilde", 2, "matilde.key-2", false},
{"matilde", 0, "matilde.key-1", false},
{"matilde", 4, "", true},
}
cache := &cache{dir}
gk := mockSchlosser{}
for _, tt := range testTable {
out, err := cache.Get(gk, tt.user, "", tt.key)
if tt.err {
if err == nil {
t.Error("Expecting error")
}
} else if strings.HasPrefix(out, tt.out) {
t.Errorf("%q != %q", tt.out, out)
}
}
}
func TestCacheGetFingerprint(t *testing.T) {
dir, err := ioutil.TempDir("", "cacheFingerprint")
if err != nil {
t.Error(err)
}
defer os.RemoveAll(dir) // clean up
cache := &cache{dir}
gk := mockSchlosser{}
out, err := cache.Get(gk, "pedro", "4a:5e:4b:4d:81:2c:de:db:d5:1d:c3:f9:6e:85:d6:ad", 0)
if err != nil {
t.Error(err)
}
pubKey, err := ioutil.ReadFile(out)
expectedKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGOhBrPToSBJCblZoK44w3/ub3K6Vx39ilHB/2sJIDqLZTx8I1U2l2RD3WhwKXdqqpH6RZh0piGlWuGV/E7xOseH9qEOKZMgscdvNO9nzD8jkSlShhZQUmhWOqLPcVUDlgIubxrFRVODcFxqgJwjm+qR2X2GaHJottrn5jFhNBEYcjdnuDKXZQ7Cr+K2bOcD+pvhMI7/qtR7jKa7Q5BoRxQEsNQEZvvgJpen2CqAsnjpJXjAXttnXJnAXcyYyOe8ZOCY/tkmXWvn9Fkd1EYmK14rB8WNEe+vraNCS9tSi1PyLMJWr3XNeluLr2/y7gHSyO6xzQNoXiTDDBFW2y3VK5"
if string(pubKey) != expectedKey {
t.Errorf("Expecting %q got %q", expectedKey, pubKey)
}
}

View File

@ -1,33 +0,0 @@
package sshvault
import (
"bytes"
"encoding/base64"
"fmt"
"io/ioutil"
"github.com/ssh-vault/crypto/oaep"
)
// Close saves encrypted data to file
func (v *vault) Close(data []byte) error {
p, err := oaep.Encrypt(v.PublicKey, v.Password, []byte(""))
if err != nil {
return err
}
var payload bytes.Buffer
payload.WriteString(base64.StdEncoding.EncodeToString(p))
payload.WriteString(";")
payload.WriteString(base64.StdEncoding.EncodeToString(data))
vault := []byte(fmt.Sprintf("SSH-VAULT;AES256;%s\n%s\n",
v.Fingerprint,
v.Encode(payload.String(), 64)),
)
if v.vault != "" {
return ioutil.WriteFile(v.vault, vault, 0600)
}
_, err = fmt.Printf("%s", vault)
return err
}

View File

@ -1,189 +0,0 @@
package main
import (
"flag"
"fmt"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"regexp"
"github.com/ssh-vault/crypto"
"github.com/ssh-vault/crypto/aead"
sv "github.com/ssh-vault/ssh-vault"
)
var version string
func exit1(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
func main() {
var (
f = flag.Bool("f", false, "Print ssh key `fingerprint` or create a vault using the key matching the specified fingerprint, example:\n echo \"secret\" | ssh-vault -u <user> -f 00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff create")
k = flag.String("k", "~/.ssh/id_rsa.pub", "Public `ssh key or index` when using option -u")
o = flag.String("o", "", "Write output to `file` instead of stdout. Only for option view, example:\n ssh-vault -o /tmp/out.txt view vault.ssh")
u = flag.String("u", "", "GitHub `username or URL`, optional [-k N] where N is the key index to use, example:\n ssh-vault -u <user> create # Using first key found in github.com/<user>.keys\n ssh-vault -u <user> -k 2 create # Using second key")
v = flag.Bool("v", false, fmt.Sprintf("Print version: %s", version))
options = []string{"create", "edit", "view"}
rxFingerprint = regexp.MustCompile(`^([0-9a-f]{2}[:-]){15}([0-9a-f]{2})$`)
err error
fingerprint string
option string
outFile string
)
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [-f fingerprint] [-k key] [-o file] [-u user] [create|edit|view] vault\n\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n\n%s\n\n",
os.Args[0],
" Options:",
" create Creates a new vault, if no vault defined outputs to stdout.",
" Can read from stdin, example:",
" echo \"secret\" | ssh-vault -u <user> create",
" edit Edit an existing vault.",
" view View an existing vault, can read from stdin, example:",
" echo \"SSH-VAULT...\" | ssh-vault view",
" vault Path off the file where the output will be written, example: vault.ssh",
)
flag.PrintDefaults()
}
flag.Parse()
if *v {
fmt.Printf("%s\n", version)
os.Exit(0)
}
// only print fingerprint
if flag.NArg() < 1 && !*f {
exit1(fmt.Errorf("Missing option, use (\"%s -h\") for help.", os.Args[0]))
}
// set option to be the first argument if no -f <fingerprint> is defined
option = flag.Arg(0)
outFile = flag.Arg(1)
// using -f with fingerprint
if *f {
if flag.NArg() == 1 {
exit1(fmt.Errorf("Missing fingerprint/option, use (\"%s -h\") for help.", os.Args[0]))
}
if flag.NArg() >= 1 {
if !rxFingerprint.Match([]byte(flag.Arg(0))) {
exit1(fmt.Errorf("Bad fingerprint format, use (\"%s -h\") for help.", os.Args[0]))
}
if flag.Arg(1) != "create" {
exit1(fmt.Errorf("-f fingerprint can only be used with the %q option, use (\"%s -h\") for help.", "create", os.Args[0]))
}
// create using fingerprint
*f = false
fingerprint = flag.Arg(0)
option = flag.Arg(1)
outFile = flag.Arg(2)
flagset := make(map[string]bool)
flag.Visit(func(f *flag.Flag) { flagset[f.Name] = true })
if flagset["k"] {
exit1(fmt.Errorf("-f fingerprint have no effect when specifying key using -k, use (\"%s -h\") for help.", os.Args[0]))
}
}
}
usr, _ := user.Current()
if len(*k) > 2 {
if (*k)[:2] == "~/" {
*k = filepath.Join(usr.HomeDir, (*k)[2:])
}
}
vault, err := sv.New(fingerprint, *k, *u, option, outFile)
if err != nil {
exit1(err)
}
// ssh-keygen -f id_rsa.pub -e -m PKCS8
PKCS8, err := vault.PKCS8()
if err != nil {
exit1(err)
}
vault.PublicKey, err = vault.GetRSAPublicKey(PKCS8)
if err != nil {
exit1(err)
}
vault.Fingerprint, err = vault.GenFingerprint(PKCS8)
if err != nil {
exit1(err)
}
if *f {
fmt.Printf("%s\n", vault.Fingerprint)
os.Exit(0)
}
// check options
exit := true
for _, v := range options {
if option == v {
exit = false
break
}
}
if exit {
exit1(fmt.Errorf("Invalid option, use (\"%s -h\") for help.\n", os.Args[0]))
}
vault.Password, err = crypto.GenerateNonce(32)
if err != nil {
exit1(err)
}
switch option {
case "create":
data, err := vault.Create()
if err != nil {
exit1(err)
}
out, err := aead.Encrypt(vault.Password, data, []byte(vault.Fingerprint))
if err != nil {
exit1(err)
}
err = vault.Close(out)
if err != nil {
exit1(err)
}
case "edit":
data, err := vault.View()
if err != nil {
exit1(fmt.Errorf("Missing vault name, use (\"%s -h\") for help.\n", os.Args[0]))
}
out, err := vault.Edit(data)
if err != nil {
exit1(err)
}
out, err = aead.Encrypt(vault.Password, out, []byte(vault.Fingerprint))
if err != nil {
exit1(err)
}
err = vault.Close(out)
if err != nil {
exit1(err)
}
case "view":
out, err := vault.View()
if err != nil {
exit1(err)
}
if *o != "" {
if err := ioutil.WriteFile(*o, out, 0600); err != nil {
exit1(err)
}
} else {
fmt.Printf("%s", out)
}
}
}

View File

@ -1,49 +0,0 @@
package sshvault
import (
"bufio"
"io/ioutil"
"os"
"os/exec"
)
// Create reads from STDIN or opens $EDITOR default to vi
func (v *vault) Create() ([]byte, error) {
// check if there is someting to read on STDIN
stat, _ := os.Stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) == 0 {
scanner := bufio.NewScanner(os.Stdin)
scanner.Split(bufio.ScanBytes)
var stdin []byte
for scanner.Scan() {
stdin = append(stdin, scanner.Bytes()...)
}
if err := scanner.Err(); err != nil {
return nil, err
}
return stdin, nil
}
// use $EDITOR
tmpfile, err := ioutil.TempFile("", v.Fingerprint)
if err != nil {
return nil, err
}
defer Shred(tmpfile.Name())
editor := os.Getenv("EDITOR")
if editor == "" {
editor = "vi"
}
cmd := exec.Command(editor, tmpfile.Name())
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
err = cmd.Run()
if err != nil {
return nil, err
}
b, err := ioutil.ReadFile(tmpfile.Name())
if err != nil {
return nil, err
}
return b, nil
}

View File

@ -1,69 +0,0 @@
package sshvault
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/ssh-vault/crypto"
"github.com/ssh-vault/crypto/aead"
)
func TestCreate(t *testing.T) {
dir, err := ioutil.TempDir("", "vault")
if err != nil {
t.Error(err)
}
defer os.RemoveAll(dir) // clean up
tmpfile := filepath.Join(dir, "vault")
vault, err := New("", "test_data/id_rsa.pub", "", "create", tmpfile)
if err != nil {
t.Error(err)
}
PKCS8, err := vault.PKCS8()
if err != nil {
t.Error(err)
}
vault.PublicKey, err = vault.GetRSAPublicKey(PKCS8)
if err != nil {
t.Error(err)
}
vault.Fingerprint, err = vault.GenFingerprint(PKCS8)
if err != nil {
t.Error(err)
}
if vault.Password, err = crypto.GenerateNonce(32); err != nil {
t.Error(err)
}
os.Setenv("EDITOR", "cat")
data, err := vault.Create()
if err != nil {
t.Error(err)
}
out, err := aead.Encrypt(vault.Password, data, []byte(vault.Fingerprint))
if err != nil {
t.Error(err)
}
if err = vault.Close(out); err != nil {
t.Error(err)
}
plaintext, err := vault.View()
if err != nil {
t.Error(err)
}
if len(plaintext) != 0 {
t.Error("Expecting 0")
}
}

View File

@ -1,36 +0,0 @@
package sshvault
import (
"io/ioutil"
"os"
"os/exec"
)
// Edit opens $EDITOR default to vi
func (v *vault) Edit(data []byte) ([]byte, error) {
tmpfile, err := ioutil.TempFile("", v.Fingerprint)
if err != nil {
return nil, err
}
defer Shred(tmpfile.Name())
err = ioutil.WriteFile(tmpfile.Name(), data, 0600)
if err != nil {
return nil, err
}
editor := os.Getenv("EDITOR")
if editor == "" {
editor = "vi"
}
cmd := exec.Command(editor, tmpfile.Name())
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
err = cmd.Run()
if err != nil {
return nil, err
}
b, err := ioutil.ReadFile(tmpfile.Name())
if err != nil {
return nil, err
}
return b, nil
}

View File

@ -1,16 +0,0 @@
package sshvault
import "bytes"
// Encode return base64 string with line break every 64 chars
func (v *vault) Encode(b string, n int) []byte {
a := []rune(b)
var buffer bytes.Buffer
for i, r := range a {
buffer.WriteRune(r)
if i > 0 && (i+1)%64 == 0 {
buffer.WriteRune('\n')
}
}
return buffer.Bytes()
}

View File

@ -1,223 +0,0 @@
package sshvault
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/ssh-vault/crypto"
"github.com/ssh-vault/crypto/aead"
)
// These are done in one function to avoid declaring global variables in a test
// file.
func TestVaultFunctionsFingerprint(t *testing.T) {
dir, err := ioutil.TempDir("", "vault")
if err != nil {
t.Error(err)
}
defer os.RemoveAll(dir) // clean up
tmpfile := filepath.Join(dir, "vault")
vault, err := New("55:cd:f2:7e:4c:0b:e5:a7:6e:6c:fc:6b:8e:58:9d:15", "test_data/id_rsa.pub", "", "create", tmpfile)
if err != nil {
t.Error(err)
}
PKCS8, err := vault.PKCS8()
if err != nil {
t.Error(err)
}
vault.PublicKey, err = vault.GetRSAPublicKey(PKCS8)
if err != nil {
t.Error(err)
}
vault.Fingerprint, err = vault.GenFingerprint(PKCS8)
if err != nil {
t.Error(err)
}
if vault.Password, err = crypto.GenerateNonce(32); err != nil {
t.Error(err)
}
// Skip vault.Create because we don't need/want to interact with an editor
// for tests.
in := []byte("The quick brown fox jumps over the lazy dog")
out, err := aead.Encrypt(vault.Password, in, []byte(vault.Fingerprint))
if err != nil {
t.Error(err)
}
if err = vault.Close(out); err != nil {
t.Error(err)
}
enc1, err := ioutil.ReadFile(tmpfile)
if err != nil {
t.Error(err)
}
plaintext, err := vault.View()
if err != nil {
t.Error(err)
}
if !bytes.Equal(in, plaintext) {
t.Error("in != out")
}
os.Setenv("EDITOR", "cat")
edited, err := vault.Edit(plaintext)
if err != nil {
t.Error(err)
}
out, err = aead.Encrypt(vault.Password, edited, []byte(vault.Fingerprint))
if err != nil {
t.Error(err)
}
if err = vault.Close(out); err != nil {
t.Error(err)
}
plaintext, err = vault.View()
if err != nil {
t.Error(err)
}
enc2, err := ioutil.ReadFile(tmpfile)
if err != nil {
t.Error(err)
}
if !bytes.Equal(edited, plaintext) {
t.Error("edited != plaintext ")
}
if bytes.Equal(enc1, enc2) {
t.Error("Expecting different encrypted outputs")
}
}
func TestVaultFunctionsSTDOUTFingerprint(t *testing.T) {
dir, err := ioutil.TempDir("", "vault")
if err != nil {
t.Error(err)
}
defer os.RemoveAll(dir) // clean up
var testTable = []struct {
Fingerprint string
KeyPath string
Option string
}{
{"55:cd:f2:7e:4c:0b:e5:a7:6e:6c:fc:6b:8e:58:9d:15", "test_data/id_rsa.pub", "create"},
{"55:cd:f2:7e:4c:0b:e5:a7:6e:6c:fc:6b:8e:58:9d:15", "test_data/id_rsa_extra_linebreak", "create"},
}
for _, tt := range testTable {
vault, err := New(tt.Fingerprint, tt.KeyPath, "", tt.Option, "")
if err != nil {
t.Error(err)
}
PKCS8, err := vault.PKCS8()
if err != nil {
t.Error(err)
}
vault.PublicKey, err = vault.GetRSAPublicKey(PKCS8)
if err != nil {
t.Error(err)
}
vault.Fingerprint, err = vault.GenFingerprint(PKCS8)
if err != nil {
t.Error(err)
}
if vault.Password, err = crypto.GenerateNonce(32); err != nil {
t.Error(err)
}
// Skip vault.Create because we don't need/want to interact with an editor
in := []byte("The quick brown fox jumps over the lazy dog")
out, err := aead.Encrypt(vault.Password, in, []byte(vault.Fingerprint))
if err != nil {
t.Error(err)
}
rescueStdout := os.Stdout // keep backup of the real stdout
r, w, _ := os.Pipe()
os.Stdout = w
if err = vault.Close(out); err != nil {
t.Error(err)
}
w.Close()
outStdout, _ := ioutil.ReadAll(r)
os.Stdout = rescueStdout
tmpfile, err := ioutil.TempFile("", "stdout")
if err != nil {
t.Error(err)
}
tmpfile.Write([]byte(outStdout))
vault.vault = tmpfile.Name()
plaintext, err := vault.View()
if err != nil {
t.Error(err)
}
if !bytes.Equal(in, plaintext) {
t.Error("in != out")
}
}
}
func TestVaultNewFingerprint(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
expect(t, "ssh-vault", r.Header.Get("User-agent"))
fmt.Fprintln(w, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDjjM4JEyg1T8j5YICtqslLNp2UGg80CppTM3ZYu73pEmDhMwbLfdhuI56AQZgWViFsF/7QHDJPcRY2Piu38b4kizTSM0QHEOC7CTo+vnzxptlKLGT1y2mcY1P9VXzCBMSWQN9/vGasgl/sUp1zcTvVT0CjjA6k1dJM6/+aDVtCsFa851VkwbeIsWl5BAHLyL+ur5BX93/BxYnRcYl7ooheuEWWokyWJ0IwEFToPMHAthTbDn1P17wYF43oscTORsFBfkP1JLBKHPDPJCGcBgQButL/srLJf6o44fScAYL99s1dQ/Qqv31aygDmwLdKEDldNnWEaJZ+iidEiIlPtAnLYGnVVA4u+NA2p3egrUrLWmpPjMX6XSb2VRHllzCcY4vZ4F2ud2TFaYG6N+9+vRCdxB+LFcHhm7ottI4vnC5P1bbMagjmFne0+TSKrAfMCw59eiQd8yZVMoE2yPXjFOQt6EOBvB4OHv1AaVt2q0PGqSkv5vIhgsKJWx/6IUj0Kz24hDiMipFb0jL3xstvizAllpC6yF26Ju/nwF03eJJGGxJjrxYd4P5/rY6SWY3yakiUN7pUBgUK2Ok3K3/+BTy5Aag8OXcvOZJumr2X2Wn9DweQeCRjC8UqFDKALqA/3vopZ2S59V4WOg3sV94hEig/KHLISNge1Uatn+qosK2sPw==")
}))
defer ts.Close()
_, err := New("55:cd:f2:7e:4c:0b:e5:a7:6e:6c:fc:6b:8e:58:9d:15", "", ts.URL, "view", "")
if err != nil {
t.Error(err)
}
}
func TestVaultNewFingerprintBadKey(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
expect(t, "ssh-vault", r.Header.Get("User-agent"))
fmt.Fprintln(w, "ssh-rsa FOO")
}))
defer ts.Close()
_, err := New("55:cd:f2:7e:4c:0b:e5:a7:6e:6c:fc:6b:8e:58:9d:15", "", ts.URL, "view", "")
if err == nil {
t.Error("Expecting error: Use a public ssh key: illegal base64 ...")
}
}
func TestVaultNewNoKeyFingerprint(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
expect(t, "ssh-vault", r.Header.Get("User-agent"))
}))
defer ts.Close()
_, err := New("55:cd:f2:7e:4c:0b:e5:a7:6e:6c:fc:6b:8e:58:9d:XX", "", ts.URL, "view", "")
if err == nil {
t.Error("Expecting error")
}
}

View File

@ -1,9 +0,0 @@
// For platforms without managed ssh private key passwords,
// fallback to prompting the user.
package sshvault
// GetPassword calls GetPasswordPrompt
func (v *vault) GetPassword() ([]byte, error) {
return v.GetPasswordPrompt()
}

View File

@ -1,41 +0,0 @@
// +build darwin
// +build amd64
// +build arm64
// Apple's OpenSSH fork uses Keychain for private key passphrases.
// They're indexed by the absolute file path to the private key,
// e.g. ~/.ssh/id_rsa
// ssh-add -K ~/.ssh/[your-private-key]
// If the passphrase isn't in keychain, prompt the user.
package sshvault
import (
"fmt"
"path/filepath"
"github.com/ssh-vault/go-keychain"
)
// GetPassword read password from keychain or promp the user
func (v *vault) GetPassword() ([]byte, error) {
var keyPassword []byte
keyPath, err := filepath.Abs(v.key)
if err != nil {
return nil, fmt.Errorf("Error finding private key: %s", err)
}
keyPassword, err = keychain.GetGenericPassword("SSH", keyPath, "", "")
if err == nil && keyPassword != nil {
return keyPassword, nil
}
// Darn, Keychain doesn't have the password for that file. Prompt the user.
keyPassword, err = v.GetPasswordPrompt()
if err != nil {
return nil, err
}
return keyPassword, nil
}

View File

@ -1,98 +0,0 @@
// +build darwin
// +build amd64
package sshvault
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"syscall"
"testing"
"github.com/kr/pty"
"github.com/ssh-vault/go-keychain"
)
func InjectKeychainPassword(path, pw string) error {
item := keychain.NewItem()
item.SetSecClass(keychain.SecClassGenericPassword)
item.SetLabel(fmt.Sprintf("SSH: %s", path))
item.SetService("SSH")
item.SetAccount(path)
item.SetData([]byte(pw))
item.SetSynchronizable(keychain.SynchronizableNo)
return keychain.AddItem(item)
}
func DeleteKeychainPassword(path string) error {
item := keychain.NewItem()
item.SetSecClass(keychain.SecClassGenericPassword)
item.SetService("SSH")
item.SetAccount(path)
return keychain.DeleteItem(item)
}
func TestKeychain(t *testing.T) {
keyPw := "argle-bargle"
keyBadPw := "totally-bogus\n"
dir, err := ioutil.TempDir("", "vault")
if err != nil {
t.Error(err)
}
defer os.RemoveAll(dir) // clean up
tmpfile := filepath.Join(dir, "vault")
vault, err := New("", "test_data/id_rsa.pub", "", "create", tmpfile)
if err != nil {
t.Error(err)
}
keyPath, err := filepath.Abs(vault.key)
if err != nil {
t.Errorf("Error finding private key: %s", err)
}
err = InjectKeychainPassword(keyPath, keyPw)
if err != nil {
t.Errorf("Error setting up keychain for testing: %s", err)
}
defer DeleteKeychainPassword(keyPath) // clean up
pty, tty, err := pty.Open()
if err != nil {
t.Errorf("Unable to open pty: %s", err)
}
// File Descriptor magic. GetPasswordPrompt() reads the password
// from stdin. For the test, we save stdin to a spare FD,
// point stdin at the file, run the system under test, and
// finally restore the original stdin
oldStdin, _ := syscall.Dup(int(syscall.Stdin))
oldStdout, _ := syscall.Dup(int(syscall.Stdout))
syscall.Dup2(int(tty.Fd()), int(syscall.Stdin))
syscall.Dup2(int(tty.Fd()), int(syscall.Stdout))
go PtyWriteback(pty, keyBadPw)
keyPwTest, err := vault.GetPassword()
syscall.Dup2(oldStdin, int(syscall.Stdin))
syscall.Dup2(oldStdout, int(syscall.Stdout))
if err != nil {
t.Error(err)
}
if strings.Trim(string(keyPwTest), "\n") == strings.Trim(keyBadPw, "\n") {
t.Errorf("PTY-based password prompt used, not keychain!")
}
if strings.Trim(string(keyPwTest), "\n") != strings.Trim(keyPw, "\n") {
t.Errorf("keychain error: %s expected %s, got %s\n", keyPath, keyPw, keyPwTest)
}
}

View File

@ -1,20 +0,0 @@
package sshvault
import (
"fmt"
"os"
"syscall"
"golang.org/x/crypto/ssh/terminal"
)
// GetPasswordPrompt ask for key password
func (v *vault) GetPasswordPrompt() ([]byte, error) {
fmt.Fprintf(os.Stderr, "Enter the key password (%s)\n", v.key)
keyPassword, err := terminal.ReadPassword(syscall.Stdin)
if err != nil {
return nil, err
}
return keyPassword, nil
}

View File

@ -1,78 +0,0 @@
package sshvault
import (
"bufio"
"bytes"
"fmt"
"io"
"net/http"
"net/textproto"
"strings"
)
// SSHKEYS_ONLINE create new pair of keys online
const SSHKEYS_ONLINE = "https://ssh-keys.online/new"
// Schlosser interface
type Schlosser interface {
GetKey(string) ([]string, error)
}
// Locksmith implements Schlosser
type Locksmith struct {
URL string
}
// GetKey fetches ssh-key from url
func (l Locksmith) GetKey(u string) ([]string, error) {
url := u
if !isURL.MatchString(u) {
switch u {
case "new":
url = SSHKEYS_ONLINE
default:
url = fmt.Sprintf("%s/%s.keys", l.URL, u)
}
}
client := &http.Client{}
// create a new request
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("User-Agent", "ssh-vault")
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
reader := bufio.NewReader(res.Body)
tp := textproto.NewReader(reader)
keys := []string{}
rsa := bytes.Buffer{}
isRSA := false
for {
if line, err := tp.ReadLine(); err != nil {
if err == io.EOF {
if len(keys) == 0 {
return nil, fmt.Errorf("key %q not found", u)
}
return keys, nil
}
return nil, err
} else if strings.HasPrefix(line, "ssh-rsa") {
keys = append(keys, line)
} else if strings.HasPrefix(line, "-----BEGIN RSA PRIVATE KEY-----") {
isRSA = true
if _, err := rsa.WriteString(line + "\n"); err != nil {
return nil, err
}
} else if strings.HasPrefix(line, "-----END RSA PRIVATE KEY-----") {
if _, err := rsa.WriteString(line); err != nil {
return nil, err
}
return []string{rsa.String()}, nil
} else if isRSA {
if _, err := rsa.WriteString(line + "\n"); err != nil {
return nil, err
}
}
}
}

View File

@ -1,104 +0,0 @@
package sshvault
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
func TestGetKeyFoundURL(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
expect(t, "ssh-vault", r.Header.Get("User-agent"))
fmt.Fprintln(w, "ssh-rsa ABC")
}))
defer ts.Close()
l := Locksmith{}
s, err := l.GetKey(ts.URL)
if err != nil {
t.Error(err)
}
expect(t, 1, len(s))
}
func TestGetKeyFoundUser(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
expect(t, "ssh-vault", r.Header.Get("User-agent"))
fmt.Fprintln(w, "ssh-rsa ABC")
}))
defer ts.Close()
l := Locksmith{ts.URL}
s, err := l.GetKey("bob")
if err != nil {
t.Error(err)
}
expect(t, 1, len(s))
}
func TestGetKeyNotFound(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
expect(t, "ssh-vault", r.Header.Get("User-agent"))
}))
defer ts.Close()
l := Locksmith{}
s, err := l.GetKey(ts.URL)
if err == nil {
t.Errorf("Expecting error")
}
expect(t, 0, len(s))
}
func TestGetKeyMultipleKeys(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
expect(t, "ssh-vault", r.Header.Get("User-agent"))
fmt.Fprintf(w, "%s\n%s\n%s\n%s\n%s\n\n\n",
"ssh-rsa ABC",
"no key",
"ssh-rsa ABC",
"ssh-foo ABC",
"ssh-rsa end",
)
}))
defer ts.Close()
l := Locksmith{}
s, err := l.GetKey(ts.URL)
if err != nil {
t.Error(err)
}
expect(t, 3, len(s))
}
func TestGetKeyRSA(t *testing.T) {
privateKey := `-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQC7NrA42dae4ThIwCAx8IB0Cte09rQhdZ+r3T2uMZm0INdgJKhO
pMg0Wv9VcPKDE+4Aw8N8dL4TqbDN4Lk3fWyGgoMLXahRDmoMKe6o/kFyqHVxlxWe
7Uhe3BHO9XCyuQu51tGzLADNSnVxDb4hhxd4Xjpb4TT69h5djYOLldYelQIDAQAB
AoGAW+TvQSikcZ5pi0RLSVgdJVjBIwHJz3a2Jp1VjnCoWsOYFIhJ2TiHUTOti5oC
YBbjR5rQFQIU3v/3WkdJgxRctR3kKDaEcWo3TTpOk5azIDc9G4XApvtVsWKgAbRh
+VXW1+uWzgHSr5RiQoXrwPP58mVHkxjFQQJjTo2/dDonu00CQQDGvomsOrXWfEh8
13NQfoP/g9q1nK1ZProB8TsNgHZz8l3URmyOb1cpUJkp3seLRnNpwda7ugm0NlQb
Z/lsBrHbAkEA8SXCmdPZ05jPOeyow8aFoLJZahwDOKCFeKOUSvhQpNDtiK+RQZu7
YxvcCOgbNJLTKP5exTOwQGptwWqyf+p0TwJAEa4FhUK7xlbMA/8OjQyUJXjPTfSg
Hx5LYbzZ6fuRjgLzgdy573nMISrAVU8yJRuhTLknpw+HqXZjyQRY1dlKnQJBAIku
c+/SZp5K1cgb6z3EF4x9KQSF/wcduhAQ7nFfpXC9MgOJ7NYn44fT925Rq/hSdjFh
00PXzbI3WUyoh/bgx10CQQCUnAyGmBU11KFVhUYdot6Paq/wrDnIgrzzXhnjtSjS
CagNNGE7cz7xEWLO/jk7ZwB7+wvIPb9BBKXH8/UnVSpJ
-----END RSA PRIVATE KEY-----`
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
expect(t, "ssh-vault", r.Header.Get("User-agent"))
fmt.Fprintf(w, "%s", privateKey)
}))
defer ts.Close()
l := Locksmith{}
s, err := l.GetKey(ts.URL)
if err != nil {
t.Error(err)
}
expect(t, 1, len(s))
expect(t, s[0], privateKey)
}

View File

@ -1,10 +0,0 @@
module github.com/ssh-vault/ssh-vault
go 1.16
require (
github.com/kr/pty v1.1.8
github.com/ssh-vault/crypto v0.0.0-20180827065533-3700ed0e38a3
github.com/ssh-vault/ssh2pem v0.0.0-20181214124859-fdda4b3b6332
golang.org/x/crypto v0.7.0
)

View File

@ -1,46 +0,0 @@
github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/ssh-vault/crypto v0.0.0-20180827065533-3700ed0e38a3 h1:Q/aUqmjnxhOxR8z5ROmsTgP91R9chC+RMBMzGQYuuTM=
github.com/ssh-vault/crypto v0.0.0-20180827065533-3700ed0e38a3/go.mod h1:oesh+PW3FU5Y5bdf0Th4fK3leo/GWH/DMIebgyix1z0=
github.com/ssh-vault/ssh2pem v0.0.0-20181214124859-fdda4b3b6332 h1:+/x76gHpfOS4qo+HOdtAYgspkn6FeqooZWCwGRGti+s=
github.com/ssh-vault/ssh2pem v0.0.0-20181214124859-fdda4b3b6332/go.mod h1:NE7nVdQvo2awuNWh/9vuIlo4m26UIOiZbcwsk+fkLeM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -1,31 +0,0 @@
package sshvault
import "os"
//Shred securely delete a file
func Shred(file string) error {
defer os.Remove(file)
f, err := os.OpenFile(file, os.O_RDWR, 0600)
if err != nil {
return err
}
fileInfo, err := f.Stat()
if err != nil {
return err
}
zeroBytes := make([]byte, fileInfo.Size())
// fill out the new slice with 0 value
copy(zeroBytes[:], "0")
// wipe the content of the target file
_, err = f.Write([]byte(zeroBytes))
if err != nil {
return err
}
return f.Close()
}

View File

@ -1,45 +0,0 @@
package sshvault
import (
"bytes"
"io/ioutil"
"os"
"testing"
)
func TestShred(t *testing.T) {
content := []byte("temporary file's content")
tmpfile, err := ioutil.TempFile("", "shred")
if err != nil {
t.Error(err)
}
defer os.Remove(tmpfile.Name()) // clean up
if _, err := tmpfile.Write(content); err != nil {
t.Error(err)
}
if err := tmpfile.Close(); err != nil {
t.Error(err)
}
b, err := ioutil.ReadFile(tmpfile.Name())
if err != nil {
t.Error(err)
}
if !bytes.Equal(b, content) {
t.Error("content != readfile")
}
if err := Shred(tmpfile.Name()); err != nil {
t.Error(err)
}
finfo, err := os.Stat(tmpfile.Name())
if err == nil {
t.Errorf("Expecting error, finfo: %v", finfo)
}
}

View File

@ -1,110 +0,0 @@
package sshvault
import (
"crypto/md5"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/ssh-vault/ssh2pem"
)
// Vault structure
type vault struct {
Password []byte
PublicKey *rsa.PublicKey
Fingerprint string
key string
vault string
}
// GITHUB https://github.com/<username>.keys
const GITHUB = "https://github.com"
// isURL regex to match if user is an URL
var isURL = regexp.MustCompile(`^https?://`)
// New initialize vault parameters
func New(f, k, u, o, v string) (*vault, error) {
var (
err error
keyPath = k
)
cache := Cache()
s := Locksmith{GITHUB}
if u != "" {
// use -k N where N is the index to use when multiple keys
// are available
var ki int
if ki, err = strconv.Atoi(k); err != nil {
ki = 1
}
if ki <= 1 {
ki = 1
}
keyPath, err = cache.Get(s, u, f, ki)
if err != nil {
return nil, err
}
} else if !cache.IsFile(keyPath) && !isURL.MatchString(k) {
return nil, fmt.Errorf("SSH key %q not found or unable to read", keyPath)
}
switch o {
case "create":
if v != "" && cache.IsFile(v) {
return nil, fmt.Errorf("File already exists: %q", v)
}
case "view":
if isURL.MatchString(k) {
keyPath, err = cache.Get(s, k, "", 0)
if err != nil {
return nil, err
}
}
}
return &vault{
key: keyPath,
vault: v,
}, nil
}
// PKCS8 convert ssh public key to PEM PKCS8
func (v *vault) PKCS8() (*pem.Block, error) {
out, err := ssh2pem.GetPem(v.key)
if err != nil {
return nil, err
}
p, rest := pem.Decode(out)
if p == nil {
return nil, fmt.Errorf("Could not create a PEM from the ssh key, %q", rest)
}
return p, nil
}
// Fingerprint return fingerprint of ssh-key
func (v *vault) GenFingerprint(p *pem.Block) (string, error) {
fingerPrint := md5.New()
fingerPrint.Write(p.Bytes)
return strings.Replace(fmt.Sprintf("% x",
fingerPrint.Sum(nil)),
" ",
":",
-1), nil
}
// GetRSAPublicKey return rsa.PublicKey
func (v *vault) GetRSAPublicKey(p *pem.Block) (*rsa.PublicKey, error) {
pubkeyInterface, err := x509.ParsePKIXPublicKey(p.Bytes)
if err != nil {
return nil, err
}
rsaPublicKey, ok := pubkeyInterface.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("No Public key found")
}
return rsaPublicKey, nil
}

View File

@ -1,268 +0,0 @@
package sshvault
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"syscall"
"testing"
"github.com/kr/pty"
"github.com/ssh-vault/crypto"
"github.com/ssh-vault/crypto/aead"
)
// These are done in one function to avoid declaring global variables in a test
// file.
func TestVaultFunctions(t *testing.T) {
dir, err := ioutil.TempDir("", "vault")
if err != nil {
t.Error(err)
}
defer os.RemoveAll(dir) // clean up
tmpfile := filepath.Join(dir, "vault")
vault, err := New("", "test_data/id_rsa.pub", "", "create", tmpfile)
if err != nil {
t.Error(err)
}
keyPw := string("argle-bargle\n")
pty, tty, err := pty.Open()
if err != nil {
t.Errorf("Unable to open pty: %s", err)
}
// File Descriptor magic. GetPasswordPrompt() reads the password
// from stdin. For the test, we save stdin to a spare FD,
// point stdin at the file, run the system under test, and
// finally restore the original stdin
oldStdin, _ := syscall.Dup(int(syscall.Stdin))
oldStdout, _ := syscall.Dup(int(syscall.Stdout))
syscall.Dup2(int(tty.Fd()), int(syscall.Stdin))
syscall.Dup2(int(tty.Fd()), int(syscall.Stdout))
go PtyWriteback(pty, keyPw)
keyPwTest, err := vault.GetPasswordPrompt()
syscall.Dup2(oldStdin, int(syscall.Stdin))
syscall.Dup2(oldStdout, int(syscall.Stdout))
if err != nil {
t.Error(err)
}
if string(strings.Trim(keyPw, "\n")) != string(keyPwTest) {
t.Errorf("password prompt: expected %s, got %s\n", keyPw, keyPwTest)
}
PKCS8, err := vault.PKCS8()
if err != nil {
t.Error(err)
}
vault.PublicKey, err = vault.GetRSAPublicKey(PKCS8)
if err != nil {
t.Error(err)
}
vault.Fingerprint, err = vault.GenFingerprint(PKCS8)
if err != nil {
t.Error(err)
}
if vault.Password, err = crypto.GenerateNonce(32); err != nil {
t.Error(err)
}
// Skip vault.Create because we don't need/want to interact with an editor
// for tests.
in := []byte("The quick brown fox jumps over the lazy dog")
out, err := aead.Encrypt(vault.Password, in, []byte(vault.Fingerprint))
if err != nil {
t.Error(err)
}
if err = vault.Close(out); err != nil {
t.Error(err)
}
enc1, err := ioutil.ReadFile(tmpfile)
if err != nil {
t.Error(err)
}
plaintext, err := vault.View()
if err != nil {
t.Error(err)
}
if !bytes.Equal(in, plaintext) {
t.Error("in != out")
}
os.Setenv("EDITOR", "cat")
edited, err := vault.Edit(plaintext)
if err != nil {
t.Error(err)
}
out, err = aead.Encrypt(vault.Password, edited, []byte(vault.Fingerprint))
if err != nil {
t.Error(err)
}
if err = vault.Close(out); err != nil {
t.Error(err)
}
plaintext, err = vault.View()
if err != nil {
t.Error(err)
}
enc2, err := ioutil.ReadFile(tmpfile)
if err != nil {
t.Error(err)
}
if !bytes.Equal(edited, plaintext) {
t.Error("edited != plaintext ")
}
if bytes.Equal(enc1, enc2) {
t.Error("Expecting different encrypted outputs")
}
}
func TestVaultFunctionsSTDOUT(t *testing.T) {
dir, err := ioutil.TempDir("", "vault")
if err != nil {
t.Error(err)
}
defer os.RemoveAll(dir) // clean up
vault, err := New("", "test_data/id_rsa.pub", "", "create", "")
if err != nil {
t.Error(err)
}
PKCS8, err := vault.PKCS8()
if err != nil {
t.Error(err)
}
vault.PublicKey, err = vault.GetRSAPublicKey(PKCS8)
if err != nil {
t.Error(err)
}
vault.Fingerprint, err = vault.GenFingerprint(PKCS8)
if err != nil {
t.Error(err)
}
if vault.Password, err = crypto.GenerateNonce(32); err != nil {
t.Error(err)
}
// Skip vault.Create because we don't need/want to interact with an editor
in := []byte("The quick brown fox jumps over the lazy dog")
out, err := aead.Encrypt(vault.Password, in, []byte(vault.Fingerprint))
if err != nil {
t.Error(err)
}
rescueStdout := os.Stdout // keep backup of the real stdout
r, w, _ := os.Pipe()
os.Stdout = w
if err = vault.Close(out); err != nil {
t.Error(err)
}
w.Close()
outStdout, _ := ioutil.ReadAll(r)
os.Stdout = rescueStdout
tmpfile, err := ioutil.TempFile("", "stdout")
if err != nil {
t.Error(err)
}
tmpfile.Write([]byte(outStdout))
vault.vault = tmpfile.Name()
plaintext, err := vault.View()
if err != nil {
t.Error(err)
}
if !bytes.Equal(in, plaintext) {
t.Error("in != out")
}
}
func TestVaultNew(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
expect(t, "ssh-vault", r.Header.Get("User-agent"))
fmt.Fprintln(w, "ssh-rsa ABC")
}))
defer ts.Close()
_, err := New("", "", ts.URL, "view", "")
if err != nil {
t.Error(err)
}
}
func TestVaultNewNoKey(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
expect(t, "ssh-vault", r.Header.Get("User-agent"))
}))
defer ts.Close()
_, err := New("", "", ts.URL, "view", "")
if err == nil {
t.Error("Expecting error")
}
}
func TestVaultNoKey(t *testing.T) {
_, err := New("", "/dev/null/none", "", "", "")
if err == nil {
t.Error("Expecting error")
}
}
func TestExistingVault(t *testing.T) {
_, err := New("", "test_data/id_rsa.pub", "", "create", "LICENSE")
if err == nil {
t.Error("Expecting error")
}
}
func TestPKCS8(t *testing.T) {
v := &vault{
key: "/dev/null/non-existent",
}
_, err := v.PKCS8()
if err == nil {
t.Error("Expecting error")
}
}
func TestKeyHTTPNotFound(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
expect(t, "ssh-vault", r.Header.Get("User-agent"))
}))
defer ts.Close()
_, err := New("", ts.URL, "", "view", "")
if err == nil {
t.Error("Expecting error")
}
}

View File

@ -1,115 +0,0 @@
package sshvault
import (
"bufio"
"bytes"
"crypto/rsa"
"encoding/base64"
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/ssh-vault/crypto/aead"
"github.com/ssh-vault/crypto/oaep"
"golang.org/x/crypto/ssh"
)
// View decrypts data and print it to stdout
func (v *vault) View() ([]byte, error) {
var (
header []string
rawPayload bytes.Buffer
scanner *bufio.Scanner
)
// check if there is someting to read on STDIN
stat, _ := os.Stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) == 0 {
scanner = bufio.NewScanner(os.Stdin)
} else {
file, err := os.Open(v.vault)
if err != nil {
return nil, fmt.Errorf("missing vault name, use (\"%s -h\") for help", os.Args[0])
}
defer file.Close()
scanner = bufio.NewScanner(file)
}
scanner.Split(bufio.ScanLines)
l := 1
for scanner.Scan() {
line := scanner.Text()
if l == 1 {
header = strings.Split(line, ";")
} else {
rawPayload.WriteString(line)
}
l++
}
// ssh-vault;AES256;fingerprint
if len(header) != 3 {
return nil, fmt.Errorf("bad ssh-vault signature, verify the input")
}
// password, body
payload := strings.Split(rawPayload.String(), ";")
if len(payload) != 2 {
return nil, fmt.Errorf("bad ssh-vault payload, verify the input")
}
// use private key only
if strings.HasSuffix(v.key, ".pub") {
v.key = strings.Trim(v.key, ".pub")
}
keyFile, err := ioutil.ReadFile(v.key)
if err != nil {
return nil, fmt.Errorf("Error reading private key: %s", err)
}
block, _ := pem.Decode(keyFile)
if block == nil || !strings.HasSuffix(block.Type, "PRIVATE KEY") {
return nil, fmt.Errorf("No valid PEM (private key) data found")
}
var privateKey interface{}
privateKey, err = ssh.ParseRawPrivateKey(keyFile)
if err, ok := err.(*ssh.PassphraseMissingError); ok {
keyPassword, err := v.GetPassword()
if err != nil {
return nil, fmt.Errorf("unable to get private key password, Decryption failed")
}
privateKey, err = ssh.ParseRawPrivateKeyWithPassphrase(keyFile, keyPassword)
if err != nil {
return nil, fmt.Errorf("could not parse private key: %v", err)
}
} else if err != nil {
return nil, fmt.Errorf("could not parse private key: %v", err)
}
ciphertext, err := base64.StdEncoding.DecodeString(payload[0])
if err != nil {
return nil, err
}
v.Password, err = oaep.Decrypt(privateKey.(*rsa.PrivateKey), ciphertext, []byte(""))
if err != nil {
return nil, fmt.Errorf("Decryption failed, use private key with fingerprint: %s", header[2])
}
ciphertext, err = base64.StdEncoding.DecodeString(payload[1])
if err != nil {
return nil, err
}
// decrypt ciphertext using fingerprint as additionalData
data, err := aead.Decrypt(v.Password, ciphertext, []byte(header[2]))
if err != nil {
return nil, err
}
return data, nil
}

29
src/bin/ssh-vault.rs Normal file
View File

@ -0,0 +1,29 @@
use anyhow::Result;
use ssh_vault::cli::{actions, actions::Action, start};
fn main() -> Result<()> {
let action = start()?;
match action {
Action::Fingerprint { .. } => {
actions::fingerprint::handle(action)?;
}
Action::Create { .. } => {
actions::create::handle(action)?;
}
Action::View { .. } => {
actions::view::handle(action)?;
}
Action::Edit { .. } => {
actions::edit::handle(action)?;
}
Action::Help => {
eprintln!("No command or argument provided, try --help");
// Exit the program with status code 1
std::process::exit(1);
}
}
Ok(())
}

125
src/cache.rs Normal file
View File

@ -0,0 +1,125 @@
use crate::tools::get_home;
use anyhow::{anyhow, Result};
use std::{
fs,
path::{Path, PathBuf},
time::{Duration, SystemTime},
};
// Load the response from a cache file ~/.ssh/vault/keys/<key>
/// # Errors
/// Return an error if the cache is older than 30 days
pub fn get(key: &str) -> Result<String> {
let cache = get_cache_path(key)?;
if cache.exists() {
let metadata = fs::metadata(&cache);
let last_modified = metadata.map_or_else(
|_| SystemTime::now(),
|meta| meta.modified().unwrap_or_else(|_| SystemTime::now()),
);
// Calculate the duration since the file was last modified
let duration_since_modified = SystemTime::now()
.duration_since(last_modified)
.unwrap_or(Duration::from_secs(0));
// Return an error if the cache is older than 30 days
if duration_since_modified > Duration::from_secs(30 * 24 * 60 * 60) {
Err(anyhow!("cache expired"))
} else {
Ok(fs::read_to_string(cache)?)
}
} else {
Err(anyhow!("cache not found"))
}
}
/// Save the response to a cache file ~/.ssh/vault/keys/<key>
/// # Errors
/// Return an error if the cache file can't be created
pub fn put(key: &str, response: &str) -> Result<()> {
let cache = get_cache_path(key)?;
// Create parent directories if they don't exist
if let Some(parent_dir) = std::path::Path::new(&cache).parent() {
fs::create_dir_all(parent_dir)?;
}
Ok(fs::write(cache, response)?)
}
/// Get the path to the cache file ~/.ssh/vault/keys/<key>
/// # Errors
/// Return an error if we can't get the path to the cache file
fn get_cache_path(key: &str) -> Result<PathBuf> {
let ssh_vault = get_ssh_vault_path()?;
Ok(ssh_vault.join("keys").join(key))
}
/// Get the path to the ssh-vault directory ~/.ssh/vault
/// # Errors
/// Return an error if we can't get the path to the ssh-vault directory
fn get_ssh_vault_path() -> Result<PathBuf> {
let home = get_home()?;
Ok(Path::new(&home).join(".ssh").join("vault"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_get_cache_path() {
let cache = get_cache_path("test").unwrap();
assert_eq!(cache.is_dir(), false);
assert_eq!(
cache.to_str(),
get_home()
.unwrap()
.join(".ssh")
.join("vault")
.join("keys")
.join("test")
.to_str()
);
}
#[test]
fn test_get_ssh_vault_path() {
let ssh_vault = get_ssh_vault_path().unwrap();
assert_eq!(ssh_vault.is_file(), false);
assert_eq!(
ssh_vault.to_str(),
get_home().unwrap().join(".ssh").join("vault").to_str()
);
}
#[test]
fn test_put() {
let cache = get_cache_path("test-2").unwrap();
put("test-2", "test").unwrap();
assert_eq!(cache.is_file(), true);
assert_eq!(cache.is_dir(), false);
assert_eq!(cache.exists(), true);
assert_eq!(
cache.to_str(),
get_home()
.unwrap()
.join(".ssh")
.join("vault")
.join("keys")
.join("test-2")
.to_str()
);
fs::remove_file(cache).unwrap();
}
#[test]
fn test_get() {
let cache = get_cache_path("test-3").unwrap();
put("test-3", "test").unwrap();
let response = get("test-3").unwrap();
assert_eq!(response, "test");
fs::remove_file(cache).unwrap();
}
}

13
src/cli/-u Normal file
View File

@ -0,0 +1,13 @@
SSH-VAULT;AES256;fd:c9:a5:ab:67:c2:6a:3b:6b:c9:72:d6:32:f8:a8:09
j2a80Cr54/JMox5X3rLDUuMBtvo3ldcYtm9zvI6ahypQAx2n/AE+e0TbFzgv35KN
wSKoaWaA1KqAuR2mEDO4gdPi2Kr2YkweBLUFf1q3sOd4WA2VkxREkHdbgYs6XG+D
+FcQx8WHibdes+/+zM8LfN2nGdPMGaYc+nICbow+osqClLM8VuPkUv+l1Fj4vzo7
sHKUGvwpcOt9Cs9rQcxuxqiRzBjSPFi1FCO2G/il09rYrqm/fhkcUC/wjTiwYe62
TbTSbLMW7ZI19jMgw0nZkHUslzliXBYzEZH1m4UhvEWstWXgafcgzFXUT2lPGprx
625zlfTHr9iCTSdaKxEWpYRdi1G3hkAg/iLAhN6JO/x4TOKeoNjilqdZ/ts4whDV
djpvYiEhOfcC74EVNS8849iugANSWr383DGN8AH0+oB/1MRkP67MmQXM7PtzgRxt
x3hV6F7MALP67hfkCq4vJf2cEGemUOrPnCl972K1NDlx85RR8aOmrFKwnF2XkH5A
EsUVYuLqtyN0mc2VXmbdWBEEDjexLbvrXuGhHcuq1psIDvLiBKlRJp/m2NmpkJsc
CDV59CVaIiG4Ol4aE5hWlgmuAHpemYzndyuOa/SRxd/wcA9pQIXWbQhwr94f5tRZ
8tN4VRN5/6Ia+m4lWGwbChqG6JOrCuwsnch/Gj7u/9s=;OAEIqzVOHe2tRxLju7D
o1BOkfrB8w85Lkf+GvFsch8T5XVc=

92
src/cli/actions/create.rs Normal file
View File

@ -0,0 +1,92 @@
use crate::cli::actions::Action;
use crate::{
tools,
vault::{crypto, find, online, remote, SshVault},
};
use anyhow::Result;
use secrecy::Secret;
use ssh_key::PublicKey;
use std::{
env, fs,
io::{Read, Write},
path::PathBuf,
process::Command,
};
use tempfile::Builder;
/// Handle the create action
pub fn handle(action: Action) -> Result<()> {
match action {
Action::Create {
fingerprint,
key,
user,
vault,
} => {
// print the url from where to download the key
let mut helper = String::new();
let ssh_key: PublicKey = if let Some(user) = user {
let int_key: Option<u32> = key.and_then(|s| s.parse::<u32>().ok());
let keys = remote::get_keys(&user)?;
let ssh_key = remote::get_user_key(&keys, int_key, fingerprint)?;
// if user equals "new" then we need to create a new key
helper = online::get_private_key_id(&ssh_key, &user)?;
ssh_key
} else {
find::public_key(key)?
};
let key_type = find::key_type(&ssh_key.algorithm())?;
let v = SshVault::new(&key_type, Some(ssh_key), None)?;
let mut data = Vec::new();
// isatty returns false if there's something in stdin.
let input_stdin = !atty::is(atty::Stream::Stdin);
// read from STDIN if there's something
if input_stdin {
std::io::stdin().read_to_end(&mut data)?;
} else {
let file = Builder::new()
.prefix(".vault-")
.suffix(".ssh")
.tempfile_in(tools::get_home()?)?;
let editor = env::var("EDITOR").unwrap_or_else(|_| String::from("vi"));
let status = Command::new(editor).arg(file.path()).status()?;
if !status.success() {
return Err(anyhow::anyhow!("Editor exited with non-zero status code",));
}
data = fs::read(file.path())?;
let _ = file_shred::shred_file(file.path());
}
// generate password (32 rand chars)
let password: Secret<[u8; 32]> = crypto::gen_password()?;
// create vault
let out = v.create(password, &data)?;
if let Some(vault) = vault {
let path = PathBuf::from(vault);
let mut file = fs::File::create(path)?;
file.write_all(out.as_bytes())?;
} else if helper.is_empty() {
println!("{out}");
} else {
let line = "---".repeat(3);
println!("Copy and paste this command to share the vault with others:\n\n{line}\n\necho \"{out}\" | ssh-vault view -k {helper}\n\n{line}\n");
}
}
_ => unreachable!(),
}
Ok(())
}

84
src/cli/actions/edit.rs Normal file
View File

@ -0,0 +1,84 @@
use crate::cli::actions::Action;
use crate::{
tools,
vault::{crypto, find, parse, ssh::decrypt_private_key, SshVault},
};
use anyhow::Result;
use secrecy::Secret;
use std::{
env, fs,
io::{Read, Write},
path::PathBuf,
process::Command,
};
use tempfile::Builder;
/// Handle the edit action
/// # Errors
/// Will return an error if the file cannot be read or written to
pub fn handle(action: Action) -> Result<()> {
match action {
Action::Edit {
key,
vault,
passphrase,
} => {
let mut data = String::new();
// read vault
let path = PathBuf::from(vault);
let mut file = fs::File::open(&path)?;
file.read_to_string(&mut data)?;
// parse vault
let (key_type, fingerprint, password, data) = parse(&data)?;
// find the private_key using the vault header AES256 or CHACHA20-POLY1305
let mut private_key = find::private_key_type(key, key_type)?;
// decrypt private_key if encrypted
if private_key.is_encrypted() {
private_key = decrypt_private_key(&private_key, passphrase)?;
}
// RSA or ED25519
let key_type = find::key_type(&private_key.algorithm())?;
// create vault
let vault = SshVault::new(&key_type, None, Some(private_key))?;
// decrypt vault
let data = vault.view(&password, &data, &fingerprint)?;
// write to temp file
let file = Builder::new()
.prefix(".vault-")
.suffix(".ssh")
.tempfile_in(tools::get_home()?)?;
// write data to the tempfile
file.as_file().write_all(data.as_bytes())?;
// open the file in the editor
let editor = env::var("EDITOR").unwrap_or_else(|_| String::from("vi"));
let status = Command::new(editor).arg(file.path()).status()?;
if !status.success() {
return Err(anyhow::anyhow!("Editor exited with non-zero status code",));
}
let data = fs::read(file.path())?;
let _ = file_shred::shred_file(file.path());
// generate password (32 rand chars)
let password: Secret<[u8; 32]> = crypto::gen_password()?;
let out = vault.create(password, &data)?;
let mut file = fs::File::create(path)?;
file.write_all(out.as_bytes())?;
}
_ => unreachable!(),
}
Ok(())
}

View File

@ -0,0 +1,67 @@
use crate::cli::actions::Action;
use crate::vault::{fingerprint, remote};
use anyhow::Result;
/// Handle the fingerprint action.
pub fn handle(action: Action) -> Result<()> {
match action {
Action::Fingerprint { key, user } => match (key, user) {
(Some(key), None) => {
let fingerprint = fingerprint::fingerprint(&key)?;
println!("{fingerprint}");
}
(None, Some(user)) => {
let keys = remote::get_keys(&user)?;
let fingerprints = fingerprint::get_remote_fingerprints(&keys, None)?;
let max_key_length = fingerprints
.iter()
.map(|f| f.key.len())
.max()
.unwrap_or_default();
for fingerprint in &fingerprints {
println!("{:width$}", fingerprint, width = max_key_length);
}
}
(Some(key), Some(user)) => {
let key_number: Result<u32, _> = key.parse();
let keys = remote::get_keys(&user)?;
match key_number {
Ok(key) => {
let fingerprints = fingerprint::get_remote_fingerprints(&keys, Some(key))?;
let max_key_length = fingerprints
.iter()
.map(|f| f.key.len())
.max()
.unwrap_or_default();
for fingerprint in &fingerprints {
println!("{:width$}", fingerprint, width = max_key_length);
}
}
Err(_) => {
eprintln!("When using -u, [-k N] must be a numeric key index.");
}
}
}
(None, None) => {
let fingerprints = fingerprint::fingerprints()?;
let max_key_length = fingerprints
.iter()
.map(|f| f.key.len())
.max()
.unwrap_or_default();
for fingerprint in &fingerprints {
println!("{:width$}", fingerprint, width = max_key_length);
}
}
},
_ => unreachable!(),
}
Ok(())
}

32
src/cli/actions/mod.rs Normal file
View File

@ -0,0 +1,32 @@
pub mod create;
pub mod edit;
pub mod fingerprint;
pub mod view;
use secrecy::Secret;
#[derive(Debug)]
pub enum Action {
Fingerprint {
key: Option<String>,
user: Option<String>,
},
Create {
fingerprint: Option<String>,
key: Option<String>,
user: Option<String>,
vault: Option<String>,
},
View {
key: Option<String>,
output: Option<String>,
passphrase: Option<Secret<String>>,
vault: Option<String>,
},
Edit {
key: Option<String>,
passphrase: Option<Secret<String>>,
vault: String,
},
Help,
}

62
src/cli/actions/view.rs Normal file
View File

@ -0,0 +1,62 @@
use crate::cli::actions::Action;
use crate::vault::{find, parse, ssh::decrypt_private_key, SshVault};
use anyhow::{anyhow, Result};
use std::{
fs::File,
io::{Read, Write},
path::PathBuf,
};
pub fn handle(action: Action) -> Result<()> {
match action {
Action::View {
key,
output,
vault,
passphrase,
} => {
let mut data = String::new();
// isatty returns false if there's something in stdin.
let input_stdin = !atty::is(atty::Stream::Stdin);
if input_stdin {
std::io::stdin().read_to_string(&mut data)?;
} else if let Some(vault) = &vault {
let path = PathBuf::from(vault);
let mut file = File::open(path)?;
file.read_to_string(&mut data)?;
} else {
return Err(anyhow!("No vault provided"));
}
// parse vault
let (key_type, fingerprint, password, data) = parse(&data)?;
// find the private_key using the vault header AES256 or CHACHA20-POLY1305
let mut private_key = find::private_key_type(key, key_type)?;
// decrypt private_key if encrypted
if private_key.is_encrypted() {
private_key = decrypt_private_key(&private_key, passphrase)?;
}
// RSA or ED25519
let key_type = find::key_type(&private_key.algorithm())?;
let vault = SshVault::new(&key_type, None, Some(private_key))?;
let data = vault.view(&password, &data, &fingerprint)?;
if let Some(output) = output {
let path = PathBuf::from(output);
let mut file = File::create(path)?;
file.write_all(data.as_bytes())?;
} else {
print!("{data}");
}
}
_ => unreachable!(),
}
Ok(())
}

View File

@ -0,0 +1,26 @@
use clap::{Arg, Command};
pub fn subcommand_create() -> Command {
Command::new("create")
.about("Create a new vault")
.visible_alias("c")
.arg(
Arg::new("fingerprint")
.short('f')
.long("fingerprint")
.help("Create a vault using the key matching the specified fingerprint"),
)
.arg(
Arg::new("key")
.short('k')
.long("key")
.help("Path to public ssh key or index when using option -u"),
)
.arg(
Arg::new("user")
.short('u')
.long("user")
.help("GitHub username or URL, optional [-k N] where N is the key index"),
)
.arg(Arg::new("vault").help("file to store the vault or writes to stdout if not specified"))
}

25
src/cli/commands/edit.rs Normal file
View File

@ -0,0 +1,25 @@
use clap::{Arg, Command};
pub fn subcommand_edit() -> Command {
Command::new("edit")
.about("Edit an existing vault")
.visible_alias("e")
.arg(
Arg::new("key")
.short('k')
.long("key")
.help("Path to the private ssh key to use for decyrpting"),
)
.arg(
Arg::new("passphrase")
.short('p')
.long("passphrase")
.env("SSH_VAULT_PASSPHRASE")
.help("Passphrase of the private ssh key"),
)
.arg(
Arg::new("vault")
.required(true)
.help("Path of the vault to edit"),
)
}

View File

@ -0,0 +1,19 @@
use clap::{Arg, Command};
pub fn subcommand_fingerprint() -> Command {
Command::new("fingerprint")
.about("Print the fingerprint of a public ssh key")
.visible_alias("f")
.arg(
Arg::new("key")
.short('k')
.long("key")
.help("Path to public ssh key or index when using option -u"),
)
.arg(
Arg::new("user")
.short('u')
.long("user")
.help("GitHub username or URL, optional [-k N] where N is the key index"),
)
}

20
src/cli/commands/mod.rs Normal file
View File

@ -0,0 +1,20 @@
pub mod create;
pub mod edit;
pub mod fingerprint;
pub mod view;
use clap::Command;
use std::env;
pub fn new(after_help: &str) -> Command {
let after_help_string = after_help.to_string();
Command::new("ssh-vault")
.about("encrypt/decrypt using ssh keys")
.version(env!("CARGO_PKG_VERSION"))
.after_help(after_help_string)
.subcommand(create::subcommand_create())
.subcommand(edit::subcommand_edit())
.subcommand(fingerprint::subcommand_fingerprint())
.subcommand(view::subcommand_view())
}

62
src/cli/commands/view.rs Normal file
View File

@ -0,0 +1,62 @@
use clap::{Arg, Command};
pub fn subcommand_view() -> Command {
Command::new("view")
.about("View an existing vault")
.visible_alias("v")
.arg(
Arg::new("key")
.short('k')
.long("key")
.help("Path to the private ssh key to use for decyrpting"),
)
.arg(
Arg::new("output")
.short('o')
.long("output")
.help("Write output to file instead of stdout"),
)
.arg(
Arg::new("passphrase")
.short('p')
.long("passphrase")
.env("SSH_VAULT_PASSPHRASE")
.help("Passphrase of the private ssh key"),
)
.arg(
Arg::new("vault")
.help("file to read the vault from or reads from stdin if not specified"),
)
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Command;
#[test]
fn test_subcommand_view() {
let app = Command::new("ssh-vault").subcommand(subcommand_view());
let matches = app.try_get_matches_from(vec![
"ssh-vault",
"view",
"-k",
"/path/to/id_rsa",
"-p",
"secret",
"/path/to/vault",
]);
assert!(matches.is_ok());
let m = matches
.unwrap()
.subcommand_matches("view")
.unwrap()
.to_owned();
assert_eq!(m.get_one::<String>("key").unwrap(), "/path/to/id_rsa");
assert_eq!(m.get_one::<String>("vault").unwrap(), "/path/to/vault");
assert_eq!(m.get_one::<String>("passphrase").unwrap(), "secret");
}
}

57
src/cli/dispatcher.rs Normal file
View File

@ -0,0 +1,57 @@
use crate::cli::actions::Action;
use anyhow::{Context, Result};
use secrecy::Secret;
pub fn dispatch(matches: &clap::ArgMatches) -> Result<Action> {
// Closure to return subcommand matches
let sub_m = |subcommand| -> Result<&clap::ArgMatches> {
matches
.subcommand_matches(subcommand)
.context("arguments not found")
};
match matches.subcommand_name() {
Some("fingerprint") => {
let sub_m = sub_m("fingerprint")?;
Ok(Action::Fingerprint {
key: sub_m.get_one("key").map(|s: &String| s.to_string()),
user: sub_m.get_one("user").map(|s: &String| s.to_string()),
})
}
Some("create") => {
let sub_m = sub_m("create")?;
Ok(Action::Create {
key: sub_m.get_one("key").map(|s: &String| s.to_string()),
user: sub_m.get_one("user").map(|s: &String| s.to_string()),
fingerprint: sub_m.get_one("fingerprint").map(|s: &String| s.to_string()),
vault: sub_m.get_one("vault").map(|s: &String| s.to_string()),
})
}
Some("view") => {
let sub_m = sub_m("view")?;
Ok(Action::View {
key: sub_m.get_one("key").map(|s: &String| s.to_string()),
vault: sub_m.get_one("vault").map(|s: &String| s.to_string()),
output: sub_m.get_one("output").map(|s: &String| s.to_string()),
passphrase: sub_m
.get_one("passphrase")
.map(|s: &String| Secret::new(s.to_string())),
})
}
Some("edit") => {
let sub_m = sub_m("edit")?;
Ok(Action::Edit {
key: sub_m.get_one("key").map(|s: &String| s.to_string()),
passphrase: sub_m
.get_one("passphrase")
.map(|s: &String| Secret::new(s.to_string())),
vault: sub_m
.get_one("vault")
.map(|s: &String| s.to_string())
.ok_or_else(|| anyhow::anyhow!("Vault path required"))?,
})
}
_ => Ok(Action::Help),
}
}

7
src/cli/mod.rs Normal file
View File

@ -0,0 +1,7 @@
pub mod actions;
mod start;
pub use self::start::start;
mod commands;
mod dispatcher;

11
src/cli/start.rs Normal file
View File

@ -0,0 +1,11 @@
use crate::cli::{actions::Action, commands, dispatcher};
use anyhow::Result;
/// Start the CLI
pub fn start() -> Result<Action> {
let after_help = format!("EXAMPLES: {}", 1);
let cmd = commands::new(&after_help);
let matches = cmd.get_matches();
let action = dispatcher::dispatch(&matches)?;
Ok(action)
}

4
src/lib.rs Normal file
View File

@ -0,0 +1,4 @@
pub mod cache;
pub mod cli;
pub mod tools;
pub mod vault;

34
src/tools.rs Normal file
View File

@ -0,0 +1,34 @@
use anyhow::{anyhow, Result};
use std::path::PathBuf;
pub fn get_home() -> Result<PathBuf> {
home::home_dir().map_or_else(|| Err(anyhow!("Could not find home directory")), Ok)
}
pub fn filter_fetched_keys(response: &str) -> Result<String> {
let mut filtered_keys = String::new();
for line in response.lines() {
if line.starts_with("ssh-rsa") || line.starts_with("ssh-ed25519") {
filtered_keys.push_str(line);
filtered_keys.push('\n'); // Add a newline to separate the lines
}
}
if filtered_keys.is_empty() {
Err(anyhow!("No SSH keys (ssh-rsa or ssh-ed25519) found"))
} else {
Ok(filtered_keys)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_home() {
let home = get_home().unwrap();
assert_eq!(home.is_dir(), true);
}
}

131
src/vault/crypto/aes256.rs Normal file
View File

@ -0,0 +1,131 @@
use aes_gcm::{
aead::{generic_array::GenericArray, Aead, AeadCore, KeyInit, OsRng, Payload},
Aes256Gcm,
};
use anyhow::{anyhow, Result};
use secrecy::{ExposeSecret, Secret};
pub struct Aes256Crypto {
key: Secret<[u8; 32]>,
}
impl super::Crypto for Aes256Crypto {
fn new(key: Secret<[u8; 32]>) -> Self {
Self { key }
}
// Encrypts data with a key and a fingerprint
fn encrypt(&self, data: &[u8], fingerprint: &[u8]) -> Result<Vec<u8>> {
let key = GenericArray::from_slice(self.key.expose_secret());
let cipher = Aes256Gcm::new(key);
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let payload = Payload {
msg: data,
aad: fingerprint,
};
cipher.encrypt(&nonce, payload).map_or_else(
|_| Err(anyhow!("Failed to encrypt data")),
|ciphertext| {
let mut encrypted_data = nonce.to_vec();
encrypted_data.extend_from_slice(&ciphertext);
Ok(encrypted_data)
},
)
}
// Decrypts data with a key and a fingerprint
fn decrypt(&self, data: &[u8], fingerprint: &[u8]) -> Result<Vec<u8>> {
let key = GenericArray::from_slice(self.key.expose_secret());
let cipher = Aes256Gcm::new(key);
let nonce = GenericArray::from_slice(&data[..12]);
let ciphertext = &data[12..];
let payload = Payload {
msg: ciphertext,
aad: fingerprint,
};
cipher
.decrypt(nonce, payload)
.map_or_else(|_| Err(anyhow!("Failed to decrypt data")), Ok)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vault::crypto::Crypto;
use rand::{rngs::OsRng, RngCore};
use std::collections::HashSet;
const TEST_DATA: &str = "The quick brown fox jumps over the lazy dog";
const FINGERPRINT: &str = "SHA256:hgIL5fEHz5zuOWY1CDlUuotdaUl4MvYG7vAgE4q4TzM";
#[test]
fn test_aes256() {
let mut password = [0_u8; 32];
OsRng.fill_bytes(&mut password);
let key = Secret::new(password);
let crypto = Aes256Crypto::new(key);
let encrypted_data = crypto
.encrypt(TEST_DATA.as_bytes(), FINGERPRINT.as_bytes())
.unwrap();
let decrypted_data = crypto
.decrypt(&encrypted_data, FINGERPRINT.as_bytes())
.unwrap();
assert_eq!(TEST_DATA.as_bytes(), decrypted_data)
}
#[test]
fn test_aes256_invalid_fingerprint() {
let mut password = [0_u8; 32];
OsRng.fill_bytes(&mut password);
let key = Secret::new(password);
let crypto = Aes256Crypto::new(key);
let encrypted_data = crypto
.encrypt(TEST_DATA.as_bytes(), FINGERPRINT.as_bytes())
.unwrap();
let decrypted_data = crypto.decrypt(&encrypted_data, b"SHA256:invalid_fingerprint");
assert!(decrypted_data.is_err());
}
#[test]
fn test_aes256_rand() {
let mut unique_keys = HashSet::new();
for _ in 0..1000 {
let mut rng = OsRng;
let mut key_bytes = [0u8; 32];
rng.fill_bytes(&mut key_bytes);
// Insert the key into the HashSet
let is_duplicate = !unique_keys.insert(key_bytes.clone());
// Check if it's a duplicate and assert
if is_duplicate {
assert!(false, "Duplicate key found")
}
let key = Secret::new(key_bytes);
let crypto = Aes256Crypto::new(key);
// Generate random data
let mut data = vec![0u8; 300];
rng.fill_bytes(&mut data);
// Generate random fingerprint
let mut fingerprint = vec![0u8; 100];
rng.fill_bytes(&mut fingerprint);
let encrypted_data = crypto.encrypt(&data, &fingerprint).unwrap();
let decrypted_data = crypto.decrypt(&encrypted_data, &fingerprint).unwrap();
assert_eq!(data, decrypted_data);
}
}
}

View File

@ -0,0 +1,133 @@
// use crate::vault::crypto::Crypto;
use anyhow::{anyhow, Result};
use chacha20poly1305::{
aead::{Aead, AeadCore, KeyInit, OsRng, Payload},
ChaCha20Poly1305,
};
use secrecy::{ExposeSecret, Secret};
pub struct ChaCha20Poly1305Crypto {
key: Secret<[u8; 32]>,
}
impl super::Crypto for ChaCha20Poly1305Crypto {
fn new(key: Secret<[u8; 32]>) -> Self {
Self { key }
}
// Encrypts data with a key and a fingerprint
fn encrypt(&self, data: &[u8], fingerprint: &[u8]) -> Result<Vec<u8>, anyhow::Error> {
let cipher = ChaCha20Poly1305::new(self.key.expose_secret().into());
let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
let payload = Payload {
msg: data,
aad: fingerprint,
};
cipher.encrypt(&nonce, payload).map_or_else(
|_| Err(anyhow!("Failed to encrypt data")),
|ciphertext| {
let mut encrypted_data = nonce.to_vec();
encrypted_data.extend_from_slice(&ciphertext);
Ok(encrypted_data)
},
)
}
// Decrypts data with a key and a fingerprint
fn decrypt(&self, data: &[u8], fingerprint: &[u8]) -> Result<Vec<u8>, anyhow::Error> {
let cipher = ChaCha20Poly1305::new(self.key.expose_secret().into());
let nonce = &data[..12];
let ciphertext = &data[12..];
let decrypted_data = cipher
.decrypt(
nonce.into(),
Payload {
msg: ciphertext,
aad: fingerprint,
},
)
.map_err(|err| anyhow!("Error decrypting password: {}", err))?;
Ok(decrypted_data)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vault::crypto::Crypto;
use rand::{rngs::OsRng, RngCore};
use std::collections::HashSet;
const TEST_DATA: &str = "The quick brown fox jumps over the lazy dog";
const FINGERPRINT: &str = "SHA256:hgIL5fEHz5zuOWY1CDlUuotdaUl4MvYG7vAgE4q4TzM";
#[test]
fn test_chacha20poly1305() {
let mut password = [0_u8; 32];
OsRng.fill_bytes(&mut password);
let key = Secret::new(password);
let crypto = ChaCha20Poly1305Crypto::new(key);
let encrypted_data = crypto
.encrypt(TEST_DATA.as_bytes(), FINGERPRINT.as_bytes())
.unwrap();
let decrypted_data = crypto
.decrypt(&encrypted_data, FINGERPRINT.as_bytes())
.unwrap();
assert_eq!(TEST_DATA.as_bytes(), decrypted_data);
}
#[test]
fn test_chacha20poly1305_wrong_fingerprint() {
let mut password = [0_u8; 32];
OsRng.fill_bytes(&mut password);
let key = Secret::new(password);
let crypto = ChaCha20Poly1305Crypto::new(key);
let encrypted_data = crypto
.encrypt(TEST_DATA.as_bytes(), FINGERPRINT.as_bytes())
.unwrap();
let decrypted_data = crypto.decrypt(&encrypted_data, b"SHA256:invalid_fingerprint");
assert!(decrypted_data.is_err());
}
#[test]
fn test_chacha20poly1305_rand() {
let mut unique_keys = HashSet::new();
for _ in 0..1000 {
let mut rng = OsRng;
let mut key_bytes = [0u8; 32];
rng.fill_bytes(&mut key_bytes);
// Insert the key into the HashSet
let is_duplicate = !unique_keys.insert(key_bytes.clone());
// Check if it's a duplicate and assert
if is_duplicate {
assert!(false, "Duplicate key found")
}
let key = Secret::new(key_bytes);
let crypto = ChaCha20Poly1305Crypto::new(key);
// Generate random data
let mut data = vec![0u8; 300];
rng.fill_bytes(&mut data);
// Generate random fingerprint
let mut fingerprint = vec![0u8; 100];
rng.fill_bytes(&mut fingerprint);
let encrypted_data = crypto.encrypt(&data, &fingerprint).unwrap();
let decrypted_data = crypto.decrypt(&encrypted_data, &fingerprint).unwrap();
assert_eq!(data, decrypted_data);
}
}
}

64
src/vault/crypto/mod.rs Normal file
View File

@ -0,0 +1,64 @@
pub mod aes256;
pub mod chacha20poly1305;
use anyhow::{anyhow, Result};
use hkdf::Hkdf;
use rand::{rngs::OsRng, RngCore};
use rsa::sha2;
use secrecy::Secret;
use sha2::Sha256;
// Define a trait for cryptographic algorithms
pub trait Crypto {
fn new(key: Secret<[u8; 32]>) -> Self;
fn encrypt(&self, data: &[u8], fingerprint: &[u8]) -> Result<Vec<u8>>;
fn decrypt(&self, data: &[u8], fingerprint: &[u8]) -> Result<Vec<u8>>;
}
// Generate a random password
pub fn gen_password() -> Result<Secret<[u8; 32]>> {
let mut password = [0_u8; 32];
OsRng.fill_bytes(&mut password);
Ok(Secret::new(password))
}
// HMAC key derivation function
pub fn hkdf(salt: &[u8], info: &[u8], ikm: &[u8]) -> Result<[u8; 32], anyhow::Error> {
let mut output_key_material = [0; 32];
// Expand the input keying material into an output keying material of 32 bytes
Hkdf::<Sha256>::new(Some(salt), ikm)
.expand(info, &mut output_key_material)
.map_err(|err| anyhow!("Error during HKDF expansion: {}", err))?;
Ok(output_key_material)
}
#[cfg(test)]
mod tests {
use super::*;
use hex_literal::hex;
use secrecy::ExposeSecret;
#[test]
fn test_gen_password() {
let password = gen_password().unwrap();
assert_eq!(password.expose_secret().len(), 32);
}
#[test]
fn test_hkdf() {
let ikm = hex!("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
let info = hex!("f0f1f2f3f4f5f6f7f8f9");
let salt = hex!("000102030405060708090a0b0c");
let expected = hex!(
"
3cb25f25faacd57a90434f64d0362f2a
2d2d0a90cf1a5a4c5db02d56ecc4c5bf
34007208d5b887185865
"
);
let okm = hkdf(&salt, &info, &ikm).unwrap();
assert_eq!(okm[..], expected[..32])
}
}

140
src/vault/find.rs Normal file
View File

@ -0,0 +1,140 @@
use crate::{
tools,
vault::{remote, SshKeyType},
};
use anyhow::{anyhow, Context, Result};
use ssh_key::{Algorithm, PrivateKey, PublicKey};
use std::{
fs::File,
io::Read,
path::{Path, PathBuf},
};
// find key type RSA or ED25519
pub fn key_type(key: &Algorithm) -> Result<SshKeyType> {
match key {
Algorithm::Rsa { .. } => Ok(SshKeyType::Rsa),
Algorithm::Ed25519 => Ok(SshKeyType::Ed25519),
_ => Err(anyhow::anyhow!("Unsupported ssh key type")),
}
}
// find public key type RSA or ED25519
pub fn private_key_type(key: Option<String>, key_type: &str) -> Result<PrivateKey> {
match key_type {
"AES256" => private_key(key, &SshKeyType::Rsa),
"CHACHA20-POLY1305" => private_key(key, &SshKeyType::Ed25519),
_ => Err(anyhow!("Unsupported key type")),
}
}
// find public key
pub fn public_key(key: Option<String>) -> Result<PublicKey> {
let key: PathBuf = if let Some(key) = key {
Path::new(&key).to_path_buf()
} else {
let home = tools::get_home()?;
let rsa_pub_key = home.join(".ssh").join("id_rsa.pub");
let ed25519_pub_key = home.join(".ssh").join("id_ed25519.pub");
if rsa_pub_key.exists() {
rsa_pub_key
} else if ed25519_pub_key.exists() {
ed25519_pub_key
} else {
return Err(anyhow::anyhow!("No key found"));
}
};
PublicKey::read_openssh_file(&key).context("Ensure you are passing a valid openssh public key")
}
// find private key legacy or openssh
pub fn private_key(key: Option<String>, ssh_type: &SshKeyType) -> Result<PrivateKey> {
let private_key = if let Some(key) = key {
if key.starts_with("http://") || key.starts_with("https://") {
remote::request(&key, true)?
} else {
let mut buffer = String::new();
File::open(&key)?.read_to_string(&mut buffer)?;
buffer
}
} else {
let home = tools::get_home()?;
let key_path = match ssh_type {
SshKeyType::Rsa => home.join(".ssh").join("id_rsa"),
SshKeyType::Ed25519 => home.join(".ssh").join("id_ed25519"),
};
if key_path.exists() {
let mut private_key = String::new();
File::open(key_path)?.read_to_string(&mut private_key)?;
private_key
} else {
return Err(anyhow!(
"No private key found in {}",
home.join(".ssh").display()
));
}
};
let private_key = private_key.trim();
// check if it's a legacy rsa key
if private_key.starts_with("-----BEGIN RSA PRIVATE KEY-----") {
return Err(anyhow!("Legacy RSA key not supported, use ssh-keygen -p -f <key> to convert it to openssh format"));
}
// read openssh key and return it as a PrivateKey
PrivateKey::from_openssh(private_key)
.context("Ensure you are passing a valid openssh private key")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vault::SshKeyType;
use ssh_key::Algorithm;
#[test]
fn test_key_type() {
assert_eq!(
key_type(&Algorithm::Rsa { hash: None }).unwrap(),
SshKeyType::Rsa
);
assert_eq!(key_type(&Algorithm::Ed25519).unwrap(), SshKeyType::Ed25519);
assert!(key_type(&Algorithm::Dsa).is_err());
}
#[test]
fn test_private_key_type() {
assert!(private_key_type(Some("test_data/id_rsa".to_string()), "AES256").is_ok());
assert!(private_key_type(Some("test_data/id_rsa".to_string()), "RSA").is_err());
assert!(
private_key_type(Some("test_data/ed25519".to_string()), "CHACHA20-POLY1305",).is_ok()
);
assert!(private_key_type(Some("test_data/ed25519".to_string()), "AES256").is_ok());
assert_eq!(
private_key_type(Some("test_data/ed25519".to_string()), "AES256")
.unwrap()
.algorithm(),
Algorithm::Ed25519
);
assert_eq!(
private_key_type(Some("test_data/id_rsa".to_string()), "CHACHA20-POLY1305",)
.unwrap()
.algorithm(),
Algorithm::Rsa { hash: None }
);
}
#[test]
fn test_public_key() {
assert!(public_key(Some("test_data/id_rsa.pub".to_string())).is_ok());
assert!(public_key(Some("test_data/ed25519.pub".to_string())).is_ok());
}
#[test]
fn test_private_key() {
assert!(private_key(Some("test_data/id_rsa".to_string()), &SshKeyType::Rsa).is_ok());
assert!(private_key(Some("test_data/ed25519".to_string()), &SshKeyType::Ed25519).is_ok());
}
}

211
src/vault/fingerprint.rs Normal file
View File

@ -0,0 +1,211 @@
use crate::tools;
use anyhow::{Context, Result};
use rsa::{pkcs8::EncodePublicKey, RsaPublicKey};
use ssh_key::{HashAlg, PublicKey};
use std::{fmt, fs, path::Path};
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Fingerprint {
pub key: String,
pub fingerprints: Vec<String>,
pub comment: String,
pub algorithm: String,
}
impl fmt::Display for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn format_fingerprint(fp: &str, width: usize) -> String {
format!("{:>width$} {}", "", fp, width = width)
}
// Access custom width from the formatter arguments
let custom_width = f.width().unwrap_or(self.key.len());
writeln!(
f,
"{:>width$} Type: {} Comment: {}",
self.key,
self.algorithm,
self.comment,
width = custom_width
)?;
for fp in &self.fingerprints {
writeln!(f, "{}", format_fingerprint(fp, custom_width))?;
}
Ok(())
}
}
pub fn fingerprints() -> Result<Vec<Fingerprint>> {
// Create a vector to store Fingerprint structs
let mut fingerprints: Vec<Fingerprint> = Vec::new();
let home = tools::get_home()?;
let ssh_home = Path::new(&home).join(".ssh");
if let Ok(entries) = fs::read_dir(ssh_home) {
for entry in entries.flatten() {
let path = entry.path();
if let Some(ext) = path.extension() {
if ext == "pub" {
if let Ok(key) = PublicKey::read_openssh_file(&path) {
// Create a Fingerprint instance
let mut fingerprint = Fingerprint {
key: path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
comment: key.comment().to_string(),
algorithm: key.algorithm().to_string(),
..Default::default()
};
fingerprint
.fingerprints
.push(key.fingerprint(HashAlg::Sha256).to_string());
if let Some(key_data) = key.key_data().rsa() {
let rsa_public_key = RsaPublicKey::try_from(key_data)?;
fingerprint
.fingerprints
.push(format!("MD5 {}", md5_fingerprint(&rsa_public_key)?));
}
fingerprints.push(fingerprint);
}
}
}
}
}
Ok(fingerprints)
}
pub fn fingerprint(key: &str) -> Result<Fingerprint> {
let path = Path::new(&key);
let key = PublicKey::read_openssh_file(path)
.context("Ensure you are passing a valid openssh public key")?;
let mut fingerprint = Fingerprint {
key: path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
comment: key.comment().to_string(),
algorithm: key.algorithm().to_string(),
..Default::default()
};
fingerprint
.fingerprints
.push(key.fingerprint(HashAlg::Sha256).to_string());
if let Some(key_data) = key.key_data().rsa() {
let rsa_public_key = RsaPublicKey::try_from(key_data)?;
fingerprint
.fingerprints
.push(format!("MD5 {}", md5_fingerprint(&rsa_public_key)?));
}
Ok(fingerprint)
}
// Fetch the ssh keys from GitHub
pub fn get_remote_fingerprints(keys: &str, key: Option<u32>) -> Result<Vec<Fingerprint>> {
// Get only SSH keys from the fetched keys
let keys = tools::filter_fetched_keys(keys)?;
// Create a vector to store Fingerprint structs
let mut fingerprints: Vec<Fingerprint> = Vec::new();
for (id, line) in keys.lines().enumerate() {
let u32_id = u32::try_from(id)?;
if let Some(mut key) = key {
key = key.saturating_sub(1);
if key >= u32::try_from(keys.lines().count())? {
Err(anyhow::anyhow!(
"key index not found, try -k with a value between 1 and {}",
keys.lines().count()
))?;
}
if u32_id != key {
continue;
}
}
if let Ok(key) = PublicKey::from_openssh(line) {
// Create a Fingerprint instance
let mut fingerprint = Fingerprint {
key: format!("ID: {}", id + 1),
comment: key.comment().to_string(),
algorithm: key.algorithm().to_string(),
..Default::default()
};
fingerprint
.fingerprints
.push(key.fingerprint(HashAlg::Sha256).to_string());
if let Some(key_data) = key.key_data().rsa() {
let rsa_public_key = RsaPublicKey::try_from(key_data)?;
fingerprint
.fingerprints
.push(format!("MD5 {}", md5_fingerprint(&rsa_public_key)?));
}
fingerprints.push(fingerprint);
}
}
Ok(fingerprints)
}
// Calculate the MD5 fingerprint of a RSA public key
// and format it as a colon separated string
pub fn md5_fingerprint(public_key: &RsaPublicKey) -> Result<String> {
let public_key_der = public_key.to_public_key_der()?;
let md5_fingerprint = md5::compute(public_key_der.as_bytes());
let formatted_fingerprint = format!("{md5_fingerprint:x}")
.chars()
.collect::<Vec<char>>()
.chunks(2)
.map(|chunk| chunk.iter().collect::<String>())
.collect::<Vec<String>>()
.join(":");
Ok(formatted_fingerprint)
}
#[cfg(test)]
mod tests {
use super::*;
struct Test {
key: &'static str,
fingerprint: &'static str,
}
#[test]
fn test_md5_fingerprint() {
let tests = [
Test {
key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDjjM4JEyg1T8j5YICtqslLNp2UGg80CppTM3ZYu73pEmDhMwbLfdhuI56AQZgWViFsF/7QHDJPcRY2Piu38b4kizTSM0QHEOC7CTo+vnzxptlKLGT1y2mcY1P9VXzCBMSWQN9/vGasgl/sUp1zcTvVT0CjjA6k1dJM6/+aDVtCsFa851VkwbeIsWl5BAHLyL+ur5BX93/BxYnRcYl7ooheuEWWokyWJ0IwEFToPMHAthTbDn1P17wYF43oscTORsFBfkP1JLBKHPDPJCGcBgQButL/srLJf6o44fScAYL99s1dQ/Qqv31aygDmwLdKEDldNnWEaJZ+iidEiIlPtAnLYGnVVA4u+NA2p3egrUrLWmpPjMX6XSb2VRHllzCcY4vZ4F2ud2TFaYG6N+9+vRCdxB+LFcHhm7ottI4vnC5P1bbMagjmFne0+TSKrAfMCw59eiQd8yZVMoE2yPXjFOQt6EOBvB4OHv1AaVt2q0PGqSkv5vIhgsKJWx/6IUj0Kz24hDiMipFb0jL3xstvizAllpC6yF26Ju/nwF03eJJGGxJjrxYd4P5/rY6SWY3yakiUN7pUBgUK2Ok3K3/+BTy5Aag8OXcvOZJumr2X2Wn9DweQeCRjC8UqFDKALqA/3vopZ2S59V4WOg3sV94hEig/KHLISNge1Uatn+qosK2sPw== test",
fingerprint: "55:cd:f2:7e:4c:0b:e5:a7:6e:6c:fc:6b:8e:58:9d:15",
},
Test {
key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCXsxWj7gvLUHbkUDzB6g+DfTdJbIcjH5Ge8ZZcYrTFeZ3hFL/pEfsuDf0Ut87QR0QpTFwM8SHyjKAX1rnF10Y+9ezG3Z4btHFk7SVPW0qqBwoTHFYiRqjgOcQrfQoDAhn9p/h93RCHR6gQPwj5CmDMRmnUcPV9mzjiLyqaqecAjGZj6q6O99Z5/lY2It/fCUcNW0JXBc31SiquvkkYhNjQsQgJxI5KnBMUEdVhk3ItJp8XeDbk2Kq03w0L8XcAqS2BUl4nNF4a5eMgME/tCUjSVYMvqcFIpOUsZhYNE+rt0ElbsMuehdvdLCbb2EBt+n75JgfGOsZCd96JrZiPlq55e0r5uDPz0rVtqnAWQawTtmSwa/VY7GZCf/xB2FvuqoXozWpAgzM7pypVx3JTBZwHx0xe/a0m1RA6+laQ4cCKV6FZWPV8WwUcvvxPknbDsjCeXgVQAxlXMk3pYrcGl61IPv/GaOr1QNPtUFRUuQXfgWh0F5SaU5MeI6HSGvuzooM=",
fingerprint: "19:b9:77:30:3f:99:15:b7:53:98:0d:ef:d1:8f:33:58",
},
];
for test in tests.iter() {
let public_key = PublicKey::from_openssh(test.key).unwrap();
let key_data = public_key.key_data().rsa().unwrap();
let rsa_public_key = RsaPublicKey::try_from(key_data).unwrap();
let fingerprint = md5_fingerprint(&rsa_public_key).unwrap();
assert_eq!(fingerprint, test.fingerprint);
}
}
}

180
src/vault/mod.rs Normal file
View File

@ -0,0 +1,180 @@
pub mod crypto;
pub mod find;
pub mod fingerprint;
pub mod online;
pub mod remote;
pub mod ssh;
pub mod parse;
pub use self::parse::parse;
use anyhow::Result;
use secrecy::Secret;
use ssh_key::{PrivateKey, PublicKey};
#[derive(Debug, PartialEq, Eq)]
pub enum SshKeyType {
Ed25519,
Rsa,
}
pub struct SshVault {
vault: Box<dyn Vault>,
}
impl SshVault {
pub fn new(
key_type: &SshKeyType,
public: Option<PublicKey>,
private: Option<PrivateKey>,
) -> Result<Self> {
let vault = match key_type {
SshKeyType::Ed25519 => {
Box::new(ssh::ed25519::Ed25519Vault::new(public, private)?) as Box<dyn Vault>
}
SshKeyType::Rsa => {
Box::new(ssh::rsa::RsaVault::new(public, private)?) as Box<dyn Vault>
}
};
Ok(Self { vault })
}
pub fn create(&self, password: Secret<[u8; 32]>, data: &[u8]) -> Result<String> {
self.vault.create(password, data)
}
pub fn view(&self, password: &[u8], data: &[u8], fingerprint: &str) -> Result<String> {
self.vault.view(password, data, fingerprint)
}
}
pub trait Vault {
fn new(public: Option<PublicKey>, private: Option<PrivateKey>) -> Result<Self>
where
Self: Sized;
fn create(&self, password: Secret<[u8; 32]>, data: &[u8]) -> Result<String>;
fn view(&self, password: &[u8], data: &[u8], fingerprint: &str) -> Result<String>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vault::{
crypto, parse, ssh::decrypt_private_key, ssh::ed25519::Ed25519Vault, ssh::rsa::RsaVault,
Vault,
};
use secrecy::Secret;
use ssh_key::PublicKey;
use std::path::Path;
struct Test {
public_key: &'static str,
private_key: &'static str,
passphrase: &'static str,
}
const SECRET: &str = "Take care of your thoughts, because they will become your words. Take care of your words, because they will become your actions. Take care of your actions, because they will become your habits. Take care of your habits, because they will become your destiny";
#[test]
fn test_rsa_vault() -> Result<()> {
let public_key_file = Path::new("test_data/id_rsa.pub");
let private_key_file = Path::new("test_data/id_rsa");
let public_key = PublicKey::read_openssh_file(&public_key_file)?;
let private_key = PrivateKey::read_openssh_file(&private_key_file)?;
let vault = RsaVault::new(Some(public_key), None)?;
let password: Secret<[u8; 32]> = crypto::gen_password()?;
let vault = vault.create(password, SECRET.as_ref())?;
let (_key_type, fingerprint, password, data) = parse(&vault)?;
let view = RsaVault::new(None, Some(private_key))?;
let vault = view.view(&password, &data, &fingerprint)?;
assert_eq!(vault, SECRET);
Ok(())
}
#[test]
fn test_ed25519_vault() -> Result<()> {
let public_key_file = Path::new("test_data/ed25519.pub");
let private_key_file = Path::new("test_data/ed25519");
let public_key = PublicKey::read_openssh_file(&public_key_file)?;
let private_key = PrivateKey::read_openssh_file(&private_key_file)?;
let vault = Ed25519Vault::new(Some(public_key), None)?;
let password: Secret<[u8; 32]> = crypto::gen_password()?;
let vault = vault.create(password, SECRET.as_ref())?;
let (_key_type, fingerprint, password, data) = parse(&vault)?;
let view = Ed25519Vault::new(None, Some(private_key))?;
let vault = view.view(&password, &data, &fingerprint)?;
assert_eq!(vault, SECRET);
Ok(())
}
#[test]
fn test_vault() -> Result<()> {
let tests = [
Test {
public_key: "test_data/id_rsa.pub",
private_key: "test_data/id_rsa",
passphrase: "",
},
Test {
public_key: "test_data/ed25519.pub",
private_key: "test_data/ed25519",
passphrase: "",
},
Test {
public_key: "test_data/id_rsa_password.pub",
private_key: "test_data/id_rsa_password",
// echo -n "ssh-vault" | openssl dgst -sha1
passphrase: "85990de849bb89120ea3016b6b76f6d004857cb7",
},
Test {
public_key: "test_data/ed25519_password.pub",
private_key: "test_data/ed25519_password",
// echo -n "ssh-vault" | openssl dgst -sha1
passphrase: "85990de849bb89120ea3016b6b76f6d004857cb7",
},
];
for test in tests.iter() {
// create
let public_key = test.public_key.to_string();
let public_key = find::public_key(Some(public_key))?;
let key_type = find::key_type(&public_key.algorithm())?;
let v = SshVault::new(&key_type, Some(public_key), None)?;
let password: Secret<[u8; 32]> = crypto::gen_password()?;
let vault = v.create(password, SECRET.as_ref())?;
// view
let private_key = test.private_key.to_string();
let (key_type, fingerprint, password, data) = parse(&vault)?;
let mut private_key = find::private_key_type(Some(private_key), key_type)?;
if private_key.is_encrypted() {
private_key = decrypt_private_key(
&private_key,
Some(Secret::new(test.passphrase.to_string())),
)?;
}
let key_type = find::key_type(&private_key.algorithm())?;
let v = SshVault::new(&key_type, None, Some(private_key))?;
let vault = v.view(&password, &data, &fingerprint)?;
assert_eq!(vault, SECRET);
}
Ok(())
}
}

45
src/vault/online.rs Normal file
View File

@ -0,0 +1,45 @@
use anyhow::Result;
use base58::ToBase58;
use ssh_key::{HashAlg, PublicKey};
use std::fmt::Write;
pub fn get_private_key_id(key: &PublicKey, user: &str) -> Result<String> {
match user {
"new" => {
let fingerprint = key.fingerprint(HashAlg::Sha256);
let mut url = String::from("https://ssh-keys.online/key/");
write!(url, "{}", fingerprint.as_bytes().to_base58())?;
Ok(url)
}
_ => Ok(String::new()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use ssh_key::PublicKey;
#[test]
fn test_get_private_key_id() {
let key = PublicKey::from_openssh(
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIlI7XymEyB/xiPQGEuiIzt7z5VDNDzuYpr3v6+hbyDN",
)
.unwrap();
let id = get_private_key_id(&key, "new").unwrap();
assert_eq!(
id,
"https://ssh-keys.online/key/59fKS1A4ZEQysHCbWSKUkR4n3a9pfN8g8BLwrp1eVJis"
);
}
#[test]
fn test_get_private_key_id_random_user() {
let key = PublicKey::from_openssh(
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIlI7XymEyB/xiPQGEuiIzt7z5VDNDzuYpr3v6+hbyDN",
)
.unwrap();
let id = get_private_key_id(&key, "random").unwrap();
assert_eq!(id, "");
}
}

51
src/vault/parse.rs Normal file
View File

@ -0,0 +1,51 @@
use anyhow::{anyhow, Result};
use base64ct::{Base64, Encoding};
// check if it's a valid SSH-VAULT file and return the data
pub fn parse(data: &str) -> Result<(&str, String, Vec<u8>, Vec<u8>)> {
let tokens: Vec<_> = data.split(';').collect();
if tokens[0] != "SSH-VAULT" || (tokens[1] != "AES256" && tokens[1] != "CHACHA20-POLY1305") {
return Err(anyhow!("Not a valid SSH-VAULT file"));
}
if tokens[1] == "AES256" {
if tokens.len() != 4 {
return Err(anyhow!("Not a valid SSH-VAULT file"));
}
let mut lines = tokens[2].lines();
let fingerprint = lines
.next()
.ok_or_else(|| anyhow!("Not a valid SSH-VAULT file"))?;
let password = lines.collect::<Vec<&str>>().join("");
let password = Base64::decode_vec(&password)?;
lines = tokens[3].lines();
let data = lines.collect::<Vec<&str>>().join("");
let data = Base64::decode_vec(&data)?;
return Ok((tokens[1], fingerprint.to_string(), password, data));
} else if tokens[1] == "CHACHA20-POLY1305" {
if tokens.len() != 6 {
return Err(anyhow!("Not a valid SSH-VAULT file"));
}
let fingerprint = tokens[2].lines().collect::<Vec<&str>>().join("");
let epk = tokens[3].lines().collect::<Vec<&str>>().join("");
let epk = Base64::decode_vec(&epk)?;
let password = tokens[4].lines().collect::<Vec<&str>>().join("");
let password = Base64::decode_vec(&password)?;
let mut epk_and_password = Vec::new();
epk_and_password.extend_from_slice(&epk);
epk_and_password.extend_from_slice(&password);
let data = tokens[5].lines().collect::<Vec<&str>>().join("");
let data = Base64::decode_vec(&data)?;
return Ok((tokens[1], fingerprint, epk_and_password, data));
}
Err(anyhow!("Not a valid SSH-VAULT file"))
}

292
src/vault/remote.rs Normal file
View File

@ -0,0 +1,292 @@
use crate::{cache, tools, vault::fingerprint};
use anyhow::{anyhow, Result};
use rsa::RsaPublicKey;
use ssh_key::{HashAlg, PublicKey};
use url::Url;
const GITHUB_BASE_URL: &str = "https://github.com";
const SSHKEYS_ONLINE: &str = "https://ssh-keys.online/new";
// Fetch the ssh keys from GitHub
pub fn get_keys(user: &str) -> Result<String> {
let mut cache = true;
let url = if user.starts_with("http://") || user.starts_with("https://") {
Url::parse(user)?
} else if user == "new" {
cache = false;
Url::parse(SSHKEYS_ONLINE)?
} else {
Url::parse(&format!("{GITHUB_BASE_URL}/{user}.keys"))?
};
request(url.as_str(), cache)
}
pub fn request(url: &str, cache: bool) -> Result<String> {
let url = Url::parse(url)?;
let cache_key = format!("{:x}", md5::compute(url.as_str().as_bytes()));
// load from cache
if let Ok(key) = cache::get(&cache_key) {
Ok(key)
} else {
let client = reqwest::blocking::Client::builder()
.user_agent("ssh-vault")
.build()?;
// Make a GET request
let res = client.get(url).send()?;
if res.status().is_success() {
// Read the response body
let body = res.text()?;
if cache {
cache::put(&cache_key, &body)?;
}
Ok(body)
} else {
Err(anyhow!(
"Request failed with status {}: {}",
res.status(),
res.text()?
))
}
}
}
pub fn get_user_key(
keys: &str,
key: Option<u32>,
fingerprint: Option<String>,
) -> Result<PublicKey> {
// Get only SSH keys from the fetched keys
let keys = tools::filter_fetched_keys(keys)?;
let key = key.map_or(0, |mut key| {
key = key.saturating_sub(1);
key
});
for (id, line) in keys.lines().enumerate() {
let u32_id = u32::try_from(id)?;
if key >= u32::try_from(keys.lines().count())? {
Err(anyhow!(
"key index not found, try -k with a value between 1 and {}",
keys.lines().count()
))?;
}
// parse the line as a public key
if let Ok(public_key) = PublicKey::from_openssh(line) {
// if fingerprint is provided, check if it matches
if let Some(f) = &fingerprint {
if public_key.fingerprint(HashAlg::Sha256).to_string() == *f {
return Ok(public_key);
}
// get the MD5 fingerprint
if let Some(key_data) = public_key.key_data().rsa() {
let rsa_public_key = RsaPublicKey::try_from(key_data)?;
if fingerprint::md5_fingerprint(&rsa_public_key)?.as_bytes() == f.as_bytes() {
return Ok(public_key);
}
}
} else if u32_id == key {
return Ok(public_key);
}
}
}
Err(anyhow!("key not found"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vault::fingerprint::get_remote_fingerprints;
use crate::vault::fingerprint::Fingerprint;
const KEYS: &str = "
# random comment
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDjjM4JEyg1T8j5YICtqslLNp2UGg80CppTM3ZYu73pEmDhMwbLfdhuI56AQZgWViFsF/7QHDJPcRY2Piu38b4kizTSM0QHEOC7CTo+vnzxptlKLGT1y2mcY1P9VXzCBMSWQN9/vGasgl/sUp1zcTvVT0CjjA6k1dJM6/+aDVtCsFa851VkwbeIsWl5BAHLyL+ur5BX93/BxYnRcYl7ooheuEWWokyWJ0IwEFToPMHAthTbDn1P17wYF43oscTORsFBfkP1JLBKHPDPJCGcBgQButL/srLJf6o44fScAYL99s1dQ/Qqv31aygDmwLdKEDldNnWEaJZ+iidEiIlPtAnLYGnVVA4u+NA2p3egrUrLWmpPjMX6XSb2VRHllzCcY4vZ4F2ud2TFaYG6N+9+vRCdxB+LFcHhm7ottI4vnC5P1bbMagjmFne0+TSKrAfMCw59eiQd8yZVMoE2yPXjFOQt6EOBvB4OHv1AaVt2q0PGqSkv5vIhgsKJWx/6IUj0Kz24hDiMipFb0jL3xstvizAllpC6yF26Ju/nwF03eJJGGxJjrxYd4P5/rY6SWY3yakiUN7pUBgUK2Ok3K3/+BTy5Aag8OXcvOZJumr2X2Wn9DweQeCRjC8UqFDKALqA/3vopZ2S59V4WOg3sV94hEig/KHLISNge1Uatn+qosK2sPw==
# another random comment
space
# another random comment
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCXsxWj7gvLUHbkUDzB6g+DfTdJbIcjH5Ge8ZZcYrTFeZ3hFL/pEfsuDf0Ut87QR0QpTFwM8SHyjKAX1rnF10Y+9ezG3Z4btHFk7SVPW0qqBwoTHFYiRqjgOcQrfQoDAhn9p/h93RCHR6gQPwj5CmDMRmnUcPV9mzjiLyqaqecAjGZj6q6O99Z5/lY2It/fCUcNW0JXBc31SiquvkkYhNjQsQgJxI5KnBMUEdVhk3ItJp8XeDbk2Kq03w0L8XcAqS2BUl4nNF4a5eMgME/tCUjSVYMvqcFIpOUsZhYNE+rt0ElbsMuehdvdLCbb2EBt+n75JgfGOsZCd96JrZiPlq55e0r5uDPz0rVtqnAWQawTtmSwa/VY7GZCf/xB2FvuqoXozWpAgzM7pypVx3JTBZwHx0xe/a0m1RA6+laQ4cCKV6FZWPV8WwUcvvxPknbDsjCeXgVQAxlXMk3pYrcGl61IPv/GaOr1QNPtUFRUuQXfgWh0F5SaU5MeI6HSGvuzooM= vault@ssh-vault.online
---
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINixf2m2nj8TDeazbWuemUY8ZHNg7znA7hVPN8TJLr2W
+++
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKdb5/i8sIEZ84k+LpJCAxRwxUZsP2MHFWApeB2TSUux ssh-vault
Fin
";
fn get_expected() -> Vec<Fingerprint> {
vec![
Fingerprint {
key: "ID: 1".to_string(),
fingerprints: vec![
"SHA256:12mLJQInCFoL9JOPJwPGb/FUEe459PY1yZEZqNGVZtA".to_string(),
"MD5 55:cd:f2:7e:4c:0b:e5:a7:6e:6c:fc:6b:8e:58:9d:15".to_string(),
],
comment: "".to_string(),
algorithm: "ssh-rsa".to_string(),
},
Fingerprint {
key: "ID: 2".to_string(),
fingerprints: vec![
"SHA256:O09r+CSX4Ub8S3klaRp86ahCLbBkxhbaXW7v8y/ANCI".to_string(),
"MD5 19:b9:77:30:3f:99:15:b7:53:98:0d:ef:d1:8f:33:58".to_string(),
],
comment: "vault@ssh-vault.online".to_string(),
algorithm: "ssh-rsa".to_string(),
},
Fingerprint {
key: "ID: 3".to_string(),
fingerprints: vec!["SHA256:hgIL5fEHz5zuOWY1CDlUuotdaUl4MvYG7vAgE4q4TzM".to_string()],
comment: "".to_string(),
algorithm: "ssh-ed25519".to_string(),
},
Fingerprint {
key: "ID: 4".to_string(),
fingerprints: vec!["SHA256:HcSHlMDnxnmeh6dsxdTrqOGUPp8Ei78VaF9t3ED21S8".to_string()],
comment: "ssh-vault".to_string(),
algorithm: "ssh-ed25519".to_string(),
},
]
}
#[test]
fn test_get_remote_fingerprints() {
let f = get_remote_fingerprints(KEYS, None).unwrap();
assert_eq!(f, get_expected());
}
#[test]
fn test_get_remote_fingerprints_with_key() {
for i in 1..=4 {
assert_eq!(
get_expected()[i - 1],
get_remote_fingerprints(KEYS, Some(i as u32)).unwrap()[0]
)
}
}
#[test]
fn test_get_remote_fingerprints_with_key_0_1() {
// key 0 and 1 should be the same
assert_eq!(
get_expected()[0],
get_remote_fingerprints(KEYS, Some(0)).unwrap()[0]
);
assert_eq!(
get_expected()[0],
get_remote_fingerprints(KEYS, Some(1)).unwrap()[0]
);
// ensure key 0 and 1 are not the same as key 2
assert_ne!(
get_expected()[0],
get_remote_fingerprints(KEYS, Some(2)).unwrap()[0]
);
}
#[test]
fn test_get_remote_fingerprints_with_empty_keys() {
assert!(get_remote_fingerprints(KEYS, Some(10)).is_err());
assert!(get_remote_fingerprints("", None).is_err());
assert!(get_remote_fingerprints("", Some(1)).is_err());
}
#[test]
fn test_get_user_key() {
let key = get_user_key(KEYS, Some(1), None).unwrap();
assert_eq!(
key.fingerprint(HashAlg::Sha256).to_string(),
"SHA256:12mLJQInCFoL9JOPJwPGb/FUEe459PY1yZEZqNGVZtA".to_string()
);
}
#[test]
fn test_get_user_key_3() {
let key = get_user_key(KEYS, Some(3), None).unwrap();
assert_eq!(
key.fingerprint(HashAlg::Sha256).to_string(),
"SHA256:hgIL5fEHz5zuOWY1CDlUuotdaUl4MvYG7vAgE4q4TzM".to_string()
);
}
#[test]
fn test_get_user_key_0_1() {
// key 0 and 1 should be the same
let key = get_user_key(KEYS, None, None).unwrap();
assert_eq!(
key.fingerprint(HashAlg::Sha256).to_string(),
"SHA256:12mLJQInCFoL9JOPJwPGb/FUEe459PY1yZEZqNGVZtA".to_string()
);
let key = get_user_key(KEYS, Some(0), None).unwrap();
assert_eq!(
key.fingerprint(HashAlg::Sha256).to_string(),
"SHA256:12mLJQInCFoL9JOPJwPGb/FUEe459PY1yZEZqNGVZtA".to_string()
);
let key = get_user_key(KEYS, Some(1), None).unwrap();
assert_eq!(
key.fingerprint(HashAlg::Sha256).to_string(),
"SHA256:12mLJQInCFoL9JOPJwPGb/FUEe459PY1yZEZqNGVZtA".to_string()
);
}
#[test]
fn test_get_user_key_with_fingerprint() {
let key = get_user_key(
KEYS,
None,
Some("SHA256:12mLJQInCFoL9JOPJwPGb/FUEe459PY1yZEZqNGVZtA".to_string()),
)
.unwrap();
assert_eq!(
key.fingerprint(HashAlg::Sha256).to_string(),
"SHA256:12mLJQInCFoL9JOPJwPGb/FUEe459PY1yZEZqNGVZtA".to_string()
);
}
#[test]
fn test_get_user_key_with_fingerprint_md5_rsa() {
let key = get_user_key(
KEYS,
None,
Some("55:cd:f2:7e:4c:0b:e5:a7:6e:6c:fc:6b:8e:58:9d:15".to_string()),
)
.unwrap();
assert_eq!(
key.fingerprint(HashAlg::Sha256).to_string(),
"SHA256:12mLJQInCFoL9JOPJwPGb/FUEe459PY1yZEZqNGVZtA".to_string()
);
let key = get_user_key(
KEYS,
None,
Some("19:b9:77:30:3f:99:15:b7:53:98:0d:ef:d1:8f:33:58".to_string()),
)
.unwrap();
assert_eq!(
key.fingerprint(HashAlg::Sha256).to_string(),
"SHA256:O09r+CSX4Ub8S3klaRp86ahCLbBkxhbaXW7v8y/ANCI".to_string()
);
}
#[test]
fn test_get_user_key_with_empty_keys() {
assert!(get_user_key("", Some(10), None).is_err());
assert!(get_user_key("", None, None).is_err());
assert!(get_user_key("", Some(1), None).is_err());
}
#[test]
fn test_get_user_key_with_key_out_of_range() {
assert!(get_user_key(KEYS, Some(10), None).is_err());
}
}

159
src/vault/ssh/ed25519.rs Normal file
View File

@ -0,0 +1,159 @@
use crate::vault::{
crypto, crypto::chacha20poly1305::ChaCha20Poly1305Crypto, crypto::Crypto, Vault,
};
use anyhow::{Context, Result};
use base64ct::{Base64, Encoding};
use secrecy::{ExposeSecret, Secret};
use sha2::{Digest, Sha512};
use ssh_key::{
private::{Ed25519PrivateKey, KeypairData},
public::KeyData,
HashAlg, PrivateKey, PublicKey,
};
use x25519_dalek::{EphemeralSecret, PublicKey as X25519PublicKey, StaticSecret};
pub struct Ed25519Vault {
montgomery_key: X25519PublicKey,
private_key: Option<Ed25519PrivateKey>,
public_key: PublicKey,
}
impl Vault for Ed25519Vault {
fn new(public: Option<PublicKey>, private: Option<PrivateKey>) -> Result<Self> {
match (public, private) {
(Some(public), None) => match public.key_data() {
KeyData::Ed25519(key_data) => {
let public_key = ed25519_dalek::VerifyingKey::try_from(key_data)
.context("Could not load key")?;
let montgomery_key: X25519PublicKey =
public_key.to_montgomery().to_bytes().into();
Ok(Self {
montgomery_key,
private_key: None,
public_key: public,
})
}
_ => Err(anyhow::anyhow!("Invalid key type for Ed25519Vault")),
},
(None, Some(private)) => match private.key_data() {
KeypairData::Ed25519(key_data) => {
if private.is_encrypted() {
return Err(anyhow::anyhow!("Private key is encrypted"));
}
let public_key = private.public_key().clone();
let verifying_key = ed25519_dalek::VerifyingKey::try_from(key_data.public)?;
let montgomery_key: X25519PublicKey =
verifying_key.to_montgomery().to_bytes().into();
Ok(Self {
montgomery_key,
private_key: Some(key_data.private.clone()),
public_key,
})
}
_ => Err(anyhow::anyhow!("Invalid key type for Ed25519Vault")),
},
_ => Err(anyhow::anyhow!("Missing public and private key")),
}
}
fn create(&self, password: Secret<[u8; 32]>, data: &[u8]) -> Result<String> {
let crypto = ChaCha20Poly1305Crypto::new(password.clone());
// get the fingerprint of the public key
let fingerprint = self.public_key.fingerprint(HashAlg::Sha256);
// encrypt the data with the password
let encrypted_data = crypto.encrypt(data, fingerprint.as_bytes())?;
// generate an ephemeral key pair
let e_secret = EphemeralSecret::random();
let e_public: X25519PublicKey = (&e_secret).into();
let shared_secret: StaticSecret =
(*e_secret.diffie_hellman(&self.montgomery_key).as_bytes()).into();
// the salt is the concatenation of the
// ephemeral public key and the receiver's public key
let mut salt = [0; 64];
salt[..32].copy_from_slice(e_public.as_bytes());
salt[32..].copy_from_slice(self.montgomery_key.as_bytes());
let enc_key = crypto::hkdf(&salt, fingerprint.as_bytes(), shared_secret.as_bytes())?;
// encrypt the password with the derived key
let crypto = ChaCha20Poly1305Crypto::new(Secret::new(enc_key));
let encrypted_password =
crypto.encrypt(password.expose_secret(), fingerprint.as_bytes())?;
// create vault payload
Ok(format!(
"SSH-VAULT;CHACHA20-POLY1305;{};{};{};{}",
fingerprint,
Base64::encode_string(e_public.as_bytes()),
Base64::encode_string(&encrypted_password),
Base64::encode_string(&encrypted_data)
)
.chars()
.collect::<Vec<_>>()
.chunks(64)
.map(|chunk| chunk.iter().collect::<String>())
.collect::<Vec<_>>()
.join("\n"))
}
fn view(&self, password: &[u8], data: &[u8], fingerprint: &str) -> Result<String> {
let get_fingerprint = self.public_key.fingerprint(HashAlg::Sha256);
if get_fingerprint.to_string() != fingerprint {
return Err(anyhow::anyhow!("Fingerprint mismatch, use correct key"));
}
match &self.private_key {
Some(private_key) => {
// extract the ephemeral public key
let mut epk: [u8; 32] = [0; 32];
epk.copy_from_slice(&password[0..32]);
// extract the encrypted password
let encrypted_password = &password[32..];
// decode the ephemeral public key
let epk = X25519PublicKey::from(epk);
// generate the static secret and public key
let sk: StaticSecret = {
let mut sk = [0u8; 32];
sk.copy_from_slice(&Sha512::digest(private_key.as_ref())[0..32]);
sk.into()
};
let pk = X25519PublicKey::from(&sk);
// generate the shared secret
let shared_secret: StaticSecret = (*sk.diffie_hellman(&epk).as_bytes()).into();
let mut salt = [0; 64];
salt[..32].copy_from_slice(epk.as_bytes());
salt[32..].copy_from_slice(pk.as_bytes());
let enc_key =
crypto::hkdf(&salt, get_fingerprint.as_bytes(), shared_secret.as_bytes())?;
// use the enc_key to decrypt the password
let crypto = ChaCha20Poly1305Crypto::new(Secret::new(enc_key));
let mut p: [u8; 32] = [0; 32];
let password = crypto.decrypt(encrypted_password, get_fingerprint.as_bytes())?;
p.copy_from_slice(&password[0..32]);
// decrypt the data with the derived key
let crypto = ChaCha20Poly1305Crypto::new(Secret::new(p));
let out = crypto.decrypt(data, get_fingerprint.as_bytes())?;
Ok(String::from_utf8(out)?)
}
None => Err(anyhow::anyhow!("Private key is required to view vault")),
}
}
}

21
src/vault/ssh/mod.rs Normal file
View File

@ -0,0 +1,21 @@
pub mod ed25519;
pub mod rsa;
use anyhow::{Context, Result};
use secrecy::{ExposeSecret, Secret};
use ssh_key::PrivateKey;
// Decrypts a private key with a password
pub fn decrypt_private_key(
key: &PrivateKey,
password: Option<Secret<String>>,
) -> Result<PrivateKey> {
let password = match password {
Some(password) => password,
None => Secret::new(rpassword::prompt_password("Enter ssh key passphrase: ")?),
};
// Decrypt the private key
key.decrypt(password.expose_secret())
.context("Failed to decrypt private key, wrong password?")
}

124
src/vault/ssh/rsa.rs Normal file
View File

@ -0,0 +1,124 @@
use crate::vault::{
crypto::aes256::Aes256Crypto, crypto::Crypto, fingerprint::md5_fingerprint, Vault,
};
use anyhow::{Context, Result};
use base64ct::{Base64, Encoding};
use rand::rngs::OsRng;
use rsa::{Oaep, RsaPrivateKey, RsaPublicKey};
use secrecy::{ExposeSecret, Secret};
use sha2::Sha256;
use ssh_key::{private::KeypairData, public::KeyData, PrivateKey, PublicKey};
pub struct RsaVault {
public_key: RsaPublicKey,
private_key: Option<RsaPrivateKey>,
}
impl Vault for RsaVault {
fn new(public: Option<PublicKey>, private: Option<PrivateKey>) -> Result<Self> {
match (public, private) {
(Some(public), None) => match public.key_data() {
KeyData::Rsa(key_data) => {
let public_key =
RsaPublicKey::try_from(key_data).context("Could not load key")?;
Ok(Self {
public_key,
private_key: None,
})
}
_ => Err(anyhow::anyhow!("Invalid key type for RsaVault")),
},
(None, Some(private)) => match private.key_data() {
KeypairData::Rsa(key_data) => {
if private.is_encrypted() {
return Err(anyhow::anyhow!("Private key is encrypted"));
}
let private_key = RsaPrivateKey::try_from(key_data)?;
let public_key = private_key.to_public_key();
Ok(Self {
public_key,
private_key: Some(private_key),
})
}
_ => Err(anyhow::anyhow!("Invalid key type for RsaVault")),
},
(Some(_), Some(_)) => Err(anyhow::anyhow!(
"Only one of public and private key is required"
)),
_ => Err(anyhow::anyhow!("Missing public and private key")),
}
}
fn create(&self, password: Secret<[u8; 32]>, data: &[u8]) -> Result<String> {
let crypto = Aes256Crypto::new(password.clone());
let fingerprint = md5_fingerprint(&self.public_key)?;
let encrypted_data = crypto.encrypt(data, fingerprint.as_bytes())?;
let encrypted_password =
self.public_key
.encrypt(&mut OsRng, Oaep::new::<Sha256>(), password.expose_secret())?;
// create vault payload
let payload = format!(
"{};{}",
Base64::encode_string(&encrypted_password),
Base64::encode_string(&encrypted_data)
)
.chars()
.collect::<Vec<_>>()
.chunks(64)
.map(|chunk| chunk.iter().collect::<String>())
.collect::<Vec<_>>()
.join("\n");
Ok(format!("SSH-VAULT;AES256;{fingerprint}\n{payload}"))
}
fn view(&self, password: &[u8], data: &[u8], fingerprint: &str) -> Result<String> {
let get_fingerprint = md5_fingerprint(&self.public_key)?;
if get_fingerprint != fingerprint {
return Err(anyhow::anyhow!("Fingerprint mismatch, use correct key"));
}
match &self.private_key {
Some(private_key) => {
let password: Secret<[u8; 32]> = Secret::new(
private_key
.decrypt(Oaep::new::<Sha256>(), password)?
.try_into()
.map_err(|_| anyhow::Error::msg("Invalid password"))?,
);
let crypto = Aes256Crypto::new(password);
let out = crypto.decrypt(data, fingerprint.as_bytes())?;
Ok(String::from_utf8(out)?)
}
None => Err(anyhow::anyhow!("Private key is required to view vault")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vault::Vault;
use anyhow::Result;
use ssh_key::{PrivateKey, PublicKey};
use std::path::Path;
#[test]
fn test_rsa_vault_using_both_keys() -> Result<()> {
let public_key_file = Path::new("test_data/id_rsa.pub");
let private_key_file = Path::new("test_data/id_rsa");
let public_key = PublicKey::read_openssh_file(&public_key_file)?;
let private_key = PrivateKey::read_openssh_file(&private_key_file)?;
let vault = RsaVault::new(Some(public_key), Some(private_key));
assert_eq!(vault.is_err(), true);
Ok(())
}
}

7
test_data/ed25519 Normal file
View File

@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDYsX9ptp4/Ew3ms21rnplGPGRzYO85wO4VTzfEyS69lgAAAKCawD2vmsA9
rwAAAAtzc2gtZWQyNTUxOQAAACDYsX9ptp4/Ew3ms21rnplGPGRzYO85wO4VTzfEyS69lg
AAAEALizhEkmm3IggV9BolSSarDLLb0yZR3vO3jvvDZK4+Btixf2m2nj8TDeazbWuemUY8
ZHNg7znA7hVPN8TJLr2WAAAAFnZhdWx0QHNzaC12YXVsdC5vbmxpbmUBAgMEBQYH
-----END OPENSSH PRIVATE KEY-----

1
test_data/ed25519.pub Normal file
View File

@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINixf2m2nj8TDeazbWuemUY8ZHNg7znA7hVPN8TJLr2W

View File

@ -0,0 +1,8 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCel3H6wU
q+AWWcoocLY6jFAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKdb5/i8sIEZ84k+
LpJCAxRwxUZsP2MHFWApeB2TSUuxAAAAkAvfIld52DjTzqhSk4Uf6ysClIQxzTfDcKgeN/
PmnyxpGcrKjSLsYsVfWfpnfpRpno0Bz9cgAS5Ex9HvtbECmgRcT90HWSzN2CHXJkIedxfc
QarXHVx1W7Q96w3C1kP4AXezUN1+ugUlvKP59F84kGbYhBv5e43RERD56bRueVklvhR8pT
uiO6+1kKrcmWyBoQ==
-----END OPENSSH PRIVATE KEY-----

View File

@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKdb5/i8sIEZ84k+LpJCAxRwxUZsP2MHFWApeB2TSUux ssh-vault

View File

@ -1,51 +1,38 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----

View File

@ -1,3 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDjjM4JEyg1T8j5YICtqslLNp2UGg80CppTM3ZYu73pEmDhMwbLfdhuI56AQZgWViFsF/7QHDJPcRY2Piu38b4kizTSM0QHEOC7CTo+vnzxptlKLGT1y2mcY1P9VXzCBMSWQN9/vGasgl/sUp1zcTvVT0CjjA6k1dJM6/+aDVtCsFa851VkwbeIsWl5BAHLyL+ur5BX93/BxYnRcYl7ooheuEWWokyWJ0IwEFToPMHAthTbDn1P17wYF43oscTORsFBfkP1JLBKHPDPJCGcBgQButL/srLJf6o44fScAYL99s1dQ/Qqv31aygDmwLdKEDldNnWEaJZ+iidEiIlPtAnLYGnVVA4u+NA2p3egrUrLWmpPjMX6XSb2VRHllzCcY4vZ4F2ud2TFaYG6N+9+vRCdxB+LFcHhm7ottI4vnC5P1bbMagjmFne0+TSKrAfMCw59eiQd8yZVMoE2yPXjFOQt6EOBvB4OHv1AaVt2q0PGqSkv5vIhgsKJWx/6IUj0Kz24hDiMipFb0jL3xstvizAllpC6yF26Ju/nwF03eJJGGxJjrxYd4P5/rY6SWY3yakiUN7pUBgUK2Ok3K3/+BTy5Aag8OXcvOZJumr2X2Wn9DweQeCRjC8UqFDKALqA/3vopZ2S59V4WOg3sV94hEig/KHLISNge1Uatn+qosK2sPw== test
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCXsxWj7gvLUHbkUDzB6g+DfTdJbIcjH5Ge8ZZcYrTFeZ3hFL/pEfsuDf0Ut87QR0QpTFwM8SHyjKAX1rnF10Y+9ezG3Z4btHFk7SVPW0qqBwoTHFYiRqjgOcQrfQoDAhn9p/h93RCHR6gQPwj5CmDMRmnUcPV9mzjiLyqaqecAjGZj6q6O99Z5/lY2It/fCUcNW0JXBc31SiquvkkYhNjQsQgJxI5KnBMUEdVhk3ItJp8XeDbk2Kq03w0L8XcAqS2BUl4nNF4a5eMgME/tCUjSVYMvqcFIpOUsZhYNE+rt0ElbsMuehdvdLCbb2EBt+n75JgfGOsZCd96JrZiPlq55e0r5uDPz0rVtqnAWQawTtmSwa/VY7GZCf/xB2FvuqoXozWpAgzM7pypVx3JTBZwHx0xe/a0m1RA6+laQ4cCKV6FZWPV8WwUcvvxPknbDsjCeXgVQAxlXMk3pYrcGl61IPv/GaOr1QNPtUFRUuQXfgWh0F5SaU5MeI6HSGvuzooM= vault@ssh-vault.online

View File

@ -49,6 +49,3 @@ l6weMXpYd6fYd+lLB7aOG653swQtWgFwZGFXrrdZX0MN3+8RKSbHODfX0kkdapLQ
M6PVo7g/ftYP9sf/1EoT/a9hLXrOa0zuKRiQXUeZE670jXPVlCivLhtIH64kxSDy
U/bBkUDkqjr63YZImqjB1EL3HMvZXjE8HsiK8npSHCCXhnxb9HjkaxiMUwEW
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,3 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDjjM4JEyg1T8j5YICtqslLNp2UGg80CppTM3ZYu73pEmDhMwbLfdhuI56AQZgWViFsF/7QHDJPcRY2Piu38b4kizTSM0QHEOC7CTo+vnzxptlKLGT1y2mcY1P9VXzCBMSWQN9/vGasgl/sUp1zcTvVT0CjjA6k1dJM6/+aDVtCsFa851VkwbeIsWl5BAHLyL+ur5BX93/BxYnRcYl7ooheuEWWokyWJ0IwEFToPMHAthTbDn1P17wYF43oscTORsFBfkP1JLBKHPDPJCGcBgQButL/srLJf6o44fScAYL99s1dQ/Qqv31aygDmwLdKEDldNnWEaJZ+iidEiIlPtAnLYGnVVA4u+NA2p3egrUrLWmpPjMX6XSb2VRHllzCcY4vZ4F2ud2TFaYG6N+9+vRCdxB+LFcHhm7ottI4vnC5P1bbMagjmFne0+TSKrAfMCw59eiQd8yZVMoE2yPXjFOQt6EOBvB4OHv1AaVt2q0PGqSkv5vIhgsKJWx/6IUj0Kz24hDiMipFb0jL3xstvizAllpC6yF26Ju/nwF03eJJGGxJjrxYd4P5/rY6SWY3yakiUN7pUBgUK2Ok3K3/+BTy5Aag8OXcvOZJumr2X2Wn9DweQeCRjC8UqFDKALqA/3vopZ2S59V4WOg3sV94hEig/KHLISNge1Uatn+qosK2sPw== test

39
test_data/id_rsa_password Normal file
View File

@ -0,0 +1,39 @@
-----BEGIN OPENSSH PRIVATE KEY-----
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
cPWcUaX7WNhiLBGasoHUTAVL0rA=
-----END OPENSSH PRIVATE KEY-----

View File

@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDnNveZaNYC14lyyOmbtCTS1cF7ZHBWzRr4Fp23Jc/D0iUdDMuup77J8qIBqqOG3JNR+jojvQjGl6zMR0RGMRIEewC/CrJlfGgp/hQymeHBEZNgw5ecvCSR9gGNUDbfeX34UkfM2dHMQkmztjQkRhOU1kchry0PhFSGQH2aTpkObU0EHbsX6RNjH/6iyi6fbiK9/6JmEy8WLKSbqUw7NPZMwnCau7l9RfuN4rhpHEd4GgDd4YIob1bVBTek4bwfdeqvixJJJjx6mXkqbPiqq2yCC+CXyV6DpJmCfgoVTZdCqHv3AXtnp0E3+lu6XkX2iyZe0SIArM0+uavT5DMhQ96QJeY8Vq+yakp4nf2ZmXnhK77AmqaS6bWDLVWem5XZ0X2DR3Ri5rtIWZByNgyC6gifx0wU33mzjUnCG0KuNBeCKxnBCsxdfEwBzpb/dmpJo35BHjxvA7P3FuaX/g5Rb28AS8Ksh/1pw/5VSTjb8oNlAlSskSk7GoNnku8R79SchrM= ssh-vault with password

12
test_data/vaultpw.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/sh
#
# Create a file named vault.gpg with the password for your ssh private key
# and encrypt it with your GPG public key, example:
#
# echo -n "secret" | gpg --output vault.gpg --encrypt --recipient your@email.tld
#
# Then run this script to decrypt the vault, example
#
# ssh-vault v -k ./test_data/ed25519_password -p $(vaultpw.sh)
gpg --quiet --batch --decrypt vault.gpg