mirror of
https://github.com/ssh-vault/ssh-vault.git
synced 2025-04-19 07:42:18 +03:00
🚀
This commit is contained in:
parent
4420c04017
commit
5c4ef780b5
2
.codespellrc
Normal file
2
.codespellrc
Normal file
@ -0,0 +1,2 @@
|
||||
[codespell]
|
||||
ignore-words-list = crate
|
5
.devcontainer/Dockerfile
Normal file
5
.devcontainer/Dockerfile
Normal 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/
|
19
.devcontainer/devcontainer.json
Normal file
19
.devcontainer/devcontainer.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
38
.github/workflows/build.yml
vendored
38
.github/workflows/build.yml
vendored
@ -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 }}
|
71
.github/workflows/codeql-analysis.yml
vendored
71
.github/workflows/codeql-analysis.yml
vendored
@ -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
|
4
.github/workflows/homebrew.yml
vendored
4
.github/workflows/homebrew.yml
vendored
@ -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
|
||||
|
56
.github/workflows/test.yml
vendored
56
.github/workflows/test.yml
vendored
@ -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
10
.gitignore
vendored
@ -1,7 +1,3 @@
|
||||
ssh-vault
|
||||
!*ssh-vault/
|
||||
coverage.*
|
||||
.goxc.local.json
|
||||
.goxc.json
|
||||
build
|
||||
vendor
|
||||
vault.gpg
|
||||
/target/
|
||||
**/*.rs.bk
|
||||
|
@ -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
2354
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
Cargo.toml
Normal file
38
Cargo.toml
Normal 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"] }
|
72
README.md
72
README.md
@ -2,8 +2,6 @@
|
||||
|
||||
[](https://github.com/ssh-vault/ssh-vault/actions/workflows/build.yml)
|
||||
[](https://github.com/ssh-vault/ssh-vault/actions/workflows/test.yml)
|
||||
[](https://coveralls.io/github/ssh-vault/ssh-vault?branch=develop)
|
||||
[](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:
|
||||
[  ](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
|
||||
|
||||
|
@ -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
|
68
go/Makefile
68
go/Makefile
@ -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
|
24
go/a_test.go
24
go/a_test.go
@ -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))
|
||||
}
|
130
go/cache.go
130
go/cache.go
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
33
go/close.go
33
go/close.go
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
49
go/create.go
49
go/create.go
@ -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
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
36
go/edit.go
36
go/edit.go
@ -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
|
||||
}
|
16
go/encode.go
16
go/encode.go
@ -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()
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
78
go/getkey.go
78
go/getkey.go
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
10
go/go.mod
10
go/go.mod
@ -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
|
||||
)
|
46
go/go.sum
46
go/go.sum
@ -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=
|
31
go/shred.go
31
go/shred.go
@ -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()
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
110
go/vault.go
110
go/vault.go
@ -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
|
||||
}
|
268
go/vault_test.go
268
go/vault_test.go
@ -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")
|
||||
}
|
||||
}
|
115
go/view.go
115
go/view.go
@ -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
29
src/bin/ssh-vault.rs
Normal 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
125
src/cache.rs
Normal 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
13
src/cli/-u
Normal 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
92
src/cli/actions/create.rs
Normal 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
84
src/cli/actions/edit.rs
Normal 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(())
|
||||
}
|
67
src/cli/actions/fingerprint.rs
Normal file
67
src/cli/actions/fingerprint.rs
Normal 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
32
src/cli/actions/mod.rs
Normal 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
62
src/cli/actions/view.rs
Normal 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(())
|
||||
}
|
26
src/cli/commands/create.rs
Normal file
26
src/cli/commands/create.rs
Normal 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
25
src/cli/commands/edit.rs
Normal 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"),
|
||||
)
|
||||
}
|
19
src/cli/commands/fingerprint.rs
Normal file
19
src/cli/commands/fingerprint.rs
Normal 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
20
src/cli/commands/mod.rs
Normal 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
62
src/cli/commands/view.rs
Normal 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
57
src/cli/dispatcher.rs
Normal 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
7
src/cli/mod.rs
Normal 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
11
src/cli/start.rs
Normal 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
4
src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod cache;
|
||||
pub mod cli;
|
||||
pub mod tools;
|
||||
pub mod vault;
|
34
src/tools.rs
Normal file
34
src/tools.rs
Normal 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
131
src/vault/crypto/aes256.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
133
src/vault/crypto/chacha20poly1305.rs
Normal file
133
src/vault/crypto/chacha20poly1305.rs
Normal 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
64
src/vault/crypto/mod.rs
Normal 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
140
src/vault/find.rs
Normal 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
211
src/vault/fingerprint.rs
Normal 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
180
src/vault/mod.rs
Normal 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
45
src/vault/online.rs
Normal 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
51
src/vault/parse.rs
Normal 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
292
src/vault/remote.rs
Normal 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
159
src/vault/ssh/ed25519.rs
Normal 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
21
src/vault/ssh/mod.rs
Normal 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
124
src/vault/ssh/rsa.rs
Normal 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
7
test_data/ed25519
Normal 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
1
test_data/ed25519.pub
Normal file
@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINixf2m2nj8TDeazbWuemUY8ZHNg7znA7hVPN8TJLr2W
|
8
test_data/ed25519_password
Normal file
8
test_data/ed25519_password
Normal file
@ -0,0 +1,8 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCel3H6wU
|
||||
q+AWWcoocLY6jFAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKdb5/i8sIEZ84k+
|
||||
LpJCAxRwxUZsP2MHFWApeB2TSUuxAAAAkAvfIld52DjTzqhSk4Uf6ysClIQxzTfDcKgeN/
|
||||
PmnyxpGcrKjSLsYsVfWfpnfpRpno0Bz9cgAS5Ex9HvtbECmgRcT90HWSzN2CHXJkIedxfc
|
||||
QarXHVx1W7Q96w3C1kP4AXezUN1+ugUlvKP59F84kGbYhBv5e43RERD56bRueVklvhR8pT
|
||||
uiO6+1kKrcmWyBoQ==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
1
test_data/ed25519_password.pub
Normal file
1
test_data/ed25519_password.pub
Normal file
@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKdb5/i8sIEZ84k+LpJCAxRwxUZsP2MHFWApeB2TSUux ssh-vault
|
@ -1,51 +1,38 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIJKQIBAAKCAgEA44zOCRMoNU/I+WCArarJSzadlBoPNAqaUzN2WLu96RJg4TMG
|
||||
y33YbiOegEGYFlYhbBf+0BwyT3EWNj4rt/G+JIs00jNEBxDguwk6Pr588abZSixk
|
||||
9ctpnGNT/VV8wgTElkDff7xmrIJf7FKdc3E71U9Ao4wOpNXSTOv/mg1bQrBWvOdV
|
||||
ZMG3iLFpeQQBy8i/rq+QV/d/wcWJ0XGJe6KIXrhFlqJMlidCMBBU6DzBwLYU2w59
|
||||
T9e8GBeN6LHEzkbBQX5D9SSwShzwzyQhnAYEAbrS/7KyyX+qOOH0nAGC/fbNXUP0
|
||||
Kr99WsoA5sC3ShA5XTZ1hGiWfoonRIiJT7QJy2Bp1VQOLvjQNqd3oK1Ky1pqT4zF
|
||||
+l0m9lUR5ZcwnGOL2eBdrndkxWmBujfvfr0QncQfixXB4Zu6LbSOL5wuT9W2zGoI
|
||||
5hZ3tPk0iqwHzAsOfXokHfMmVTKBNsj14xTkLehDgbweDh79QGlbdqtDxqkpL+by
|
||||
IYLCiVsf+iFI9Cs9uIQ4jIqRW9Iy98bLb4swJZaQushduibv58BdN3iSRhsSY68W
|
||||
HeD+f62OklmN8mpIlDe6VAYFCtjpNyt//gU8uQGoPDl3LzmSbpq9l9lp/Q8HkHgk
|
||||
YwvFKhQygC6gP976KWdkufVeFjoN7FfeIRIoPyhyyEjYHtVGrZ/qqLCtrD8CAwEA
|
||||
AQKCAgBlAMELoic80rdgigdUDvTen9V+Qbrx3Kr3t2YWqO75H6FFFMM1XBzPdpwK
|
||||
ThNWBtE7C8OdWIa0YHv9g8cgFPvTeL7vdrYBdOpr2wKxixgmo2rb06zUtX+hXS2Q
|
||||
y3tfG4KvNwh9yIDCnfQ6D8m3zlFCs7T7Y1W0sPxyDGceWENj7KXzn7N1z87JrAYn
|
||||
IzIw5GDBB0jI4yEgP7CW+RCPgmuJr72jyVK5a5+jefxWQXG3OpszSNEyuY3SG9Jv
|
||||
sUisTxfFHGjJzYk6vHHfYChS+xCYO/cgbI+ThUlnilRrUvh0BjLN7TXaK7lWrUeN
|
||||
JYm2JFFyIJQ1O79hV61qbDiW2lFl0IwgOUZuEe5QoyIb9ueJp6n9D8rRcUp0ITou
|
||||
hmYUHDCM/7TtwmgD0Z5MG16DiaJhwOKs3AtLiL5gu1PvJXQ5t1AfvQ23zKjF/1eP
|
||||
JqIrv+KRmO28dbGutYIqJ3rV4X00jbUnjqilOXAnJGNYQQPyOnuW3mDZTK2Oe2XA
|
||||
d+DrRMO0OOd/lc/sabUANsVRIZl5+gEpzISOReeWyJbYA8jf+tJPu8oQiAzfb9po
|
||||
Mt7TmPMhrGVNkeDG1J/rhWJ4/2kO9P/JqQ3eTJECwwUcgywL5O0wTEJj9YoyXHyG
|
||||
U7oHR6lSQ0xRJMvc5r65EUClhX1rqA7aq7LugFM9ByMwCf73gQKCAQEA9mt7b0yY
|
||||
XBP6BPqjcRLAup9rdKUjnggX7PduGDMmERr1y8uHC4TXU39ur6KLhrOLT4daoZjx
|
||||
lMKsoBqRXBeYeeXYcG+Rq3aszi+DkNGdYqN9vIAy/10Tj/+aRWK7zv4OSUQEqIvA
|
||||
XQslMJCX54InrB07olpK0MxFlokLsvUwn8j6Sy5ZKgn8Wi4Bvn6fizDVEFAbD4qn
|
||||
a9AjZjXmMtkGrhMmCen5mewVwqKd+6OTEkbqNK9i9DrKQs18G4pvZdGcXCx1FHmx
|
||||
frCVpH8RJKL4UYsl10A9DmvrrP3nVVJ5I9RinYs4sJ3TajwAYDwBKA4evG+Yz5oX
|
||||
VHsO1J3eLyvuIQKCAQEA7GWEzxoN8NhOAuXBb+8HjRYXfWKi7flie+a5LRtlcNTQ
|
||||
/8SIDePIVgVpquFVEfLgMdl0UK0EDmmhfC70SYc1avjBl4QPj7I1+zPty4W3PNcx
|
||||
shCkSoU6CdlaZE66ZKe2KwxTaudVQTQD6fkQ40t1Je8zWT21hbTnVsuX6PAid6EW
|
||||
d3Qv2G2WDSdsc3BQTMh+GMjamT75pWf+XKx6WLTwDMo0mP6ftZLL8kMzNQm7hRM5
|
||||
VlrrO7qm51QImVBfYkYKuoJ11xOjaLGXu+ZH+a28GBoJSOHkwzrvvYLBAtaGEcEs
|
||||
JQPvnTzbocxBipm0bTxHe20QXlonOdCN2+47BAqOXwKCAQB61u27d/VjwVmbbc5Y
|
||||
Kb8FyT7p8QCmYOZ8bIPncGgDkusA4r65UUl+CEKHL4Jabdp+lLzrXbDgNYOUdGoN
|
||||
/H04FwokUpnNXeWbCziM0tGgQFwHweiqQB3mZMbk2+k/sIoyn8Oqua/1Mf8iDJKN
|
||||
B+b697+omVY+a/Ysqri4R14r6SZMoBg2yq+PzOt1qO2jl34/GY/D2ZzINAPRigDt
|
||||
TB72W/lUa9zPjE70rdgPf2VrcQqDpQKxOTVmw8Sgfgw2N524nTjoMhn3S9PtIqLL
|
||||
HqqnC7SbbxyAue/MVu4hLHHTGKboXmSuNp8TkEGnt4HkE9XinccRhoEmzgmAmAoQ
|
||||
fdfBAoIBAQC+qZqWVxNLX4p798cueoGCn4DOllGG0o9GE27jHKeSe3Md9ustGLyp
|
||||
9K+nLRqp4VRFoHeZ4hHVdgjS2iQZcb35yyyJAfBzG29Cbj2q2uxRW9cvIREBMbb/
|
||||
3phzMrRPHp1k1woovPAcKNHHUiT2zhNsRyMJJSZU8vVrIcYiEBwclIZETieQzcIq
|
||||
VdO4v6tkYoihgM4er5Y2fEvBfzMmfLjn9+a4RxWVIxLvEJgqfPELgdfK+IWlpQOc
|
||||
rEBLN3HXF1rr1vEoSSSQ+jvBRxhiHmgIoGdAq0EQ3WYjWWRG37M+eqkaKbSHA2C5
|
||||
fL6YBSRqviWBPRPopZnhnX3tFpXuynzNAoIBAQCPmOLkgwh2kCHOGiCw0d09EZ49
|
||||
RqPO9xXdQ8Au5dwCMFoDO6+mC7FBsz1UDEAs4KYKJR+eOwNqM0D9SL3wpoU3ewNT
|
||||
XxZv6HKiB2vrKwLv3Ca8sBp5aOgGOFXe2Cd3OrmSnrawNlWOjCLkwbYrLIqkZz5s
|
||||
l6weMXpYd6fYd+lLB7aOG653swQtWgFwZGFXrrdZX0MN3+8RKSbHODfX0kkdapLQ
|
||||
M6PVo7g/ftYP9sf/1EoT/a9hLXrOa0zuKRiQXUeZE670jXPVlCivLhtIH64kxSDy
|
||||
U/bBkUDkqjr63YZImqjB1EL3HMvZXjE8HsiK8npSHCCXhnxb9HjkaxiMUwEW
|
||||
-----END RSA PRIVATE KEY-----
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
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
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
|
@ -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
|
||||
|
@ -49,6 +49,3 @@ l6weMXpYd6fYd+lLB7aOG653swQtWgFwZGFXrrdZX0MN3+8RKSbHODfX0kkdapLQ
|
||||
M6PVo7g/ftYP9sf/1EoT/a9hLXrOa0zuKRiQXUeZE670jXPVlCivLhtIH64kxSDy
|
||||
U/bBkUDkqjr63YZImqjB1EL3HMvZXjE8HsiK8npSHCCXhnxb9HjkaxiMUwEW
|
||||
-----END RSA PRIVATE KEY-----
|
||||
|
||||
|
||||
|
3
test_data/id_rsa_legacy.pub
Normal file
3
test_data/id_rsa_legacy.pub
Normal 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
39
test_data/id_rsa_password
Normal file
@ -0,0 +1,39 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
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
|
||||
cPWcUaX7WNhiLBGasoHUTAVL0rA=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
1
test_data/id_rsa_password.pub
Normal file
1
test_data/id_rsa_password.pub
Normal 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
12
test_data/vaultpw.sh
Executable 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
|
Loading…
x
Reference in New Issue
Block a user