From f6ff704509b338fe6c417b37a64c24fe93fb4f48 Mon Sep 17 00:00:00 2001 From: Samuel Rounce Date: Wed, 21 Dec 2022 20:19:42 +0000 Subject: [PATCH 1/5] Feat: Allow users to provide private key passphrase --- cmd/ssh-to-age/main.go | 12 ++++++---- cmd/ssh-to-age/main_test.go | 23 ++++++++++++++++--- .../test-assets/id_ed25519_passphrase | 8 +++++++ .../test-assets/id_ed25519_passphrase.pub | 1 + convert.go | 15 ++++++++---- 5 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 cmd/ssh-to-age/test-assets/id_ed25519_passphrase create mode 100644 cmd/ssh-to-age/test-assets/id_ed25519_passphrase.pub diff --git a/cmd/ssh-to-age/main.go b/cmd/ssh-to-age/main.go index 674cbf3..0dfbac9 100644 --- a/cmd/ssh-to-age/main.go +++ b/cmd/ssh-to-age/main.go @@ -12,8 +12,8 @@ import ( ) type options struct { - out, in string - privateKey bool + out, in, passphrase string + privateKey bool } func parseFlags(args []string) options { @@ -22,6 +22,7 @@ func parseFlags(args []string) options { f.BoolVar(&opts.privateKey, "private-key", false, "convert private key instead of public key") f.StringVar(&opts.in, "i", "-", "Input path. Reads by default from standard output") f.StringVar(&opts.out, "o", "-", "Output path. Prints by default to standard output") + f.StringVar(&opts.passphrase, "passphrase", "", "Output path. Prints by default to standard output") if err := f.Parse(args[1:]); err != nil { // should never happen since flag.ExitOnError panic(err) @@ -63,9 +64,12 @@ func convertKeys(args []string) error { } defer writer.Close() } - if opts.privateKey { - key, _, err := sshage.SSHPrivateKeyToAge(sshKey) + var ( + key *string + err error + ) + key, _, err = sshage.SSHPrivateKeyToAge(sshKey, []byte(opts.passphrase)) if err != nil { return fmt.Errorf("failed to convert '%s': %w", sshKey, err) } diff --git a/cmd/ssh-to-age/main_test.go b/cmd/ssh-to-age/main_test.go index 70bdf29..f6c2d3a 100644 --- a/cmd/ssh-to-age/main_test.go +++ b/cmd/ssh-to-age/main_test.go @@ -64,15 +64,15 @@ func TestSshKeyScan(t *testing.T) { file, err := os.Open(out) ok(t, err) - defer file.Close() + defer file.Close() scanner := bufio.NewScanner(file) - for scanner.Scan() { + for scanner.Scan() { pubKey := strings.TrimSuffix(scanner.Text(), "\n") fmt.Printf("scanned key: %s\n", pubKey) _, err = age.ParseX25519Recipient(pubKey) ok(t, err) - } + } ok(t, scanner.Err()) } @@ -92,3 +92,20 @@ func TestPrivateKey(t *testing.T) { _, err = age.ParseX25519Identity(privateKey) ok(t, err) } + +func TestPrivateKeyWithPassphrase(t *testing.T) { + tempdir := TempDir(t) + defer os.RemoveAll(tempdir) + out := path.Join(tempdir, "out") + + err := convertKeys([]string{"ssh-to-age", "-private-key", "-i", Asset("id_ed25519_passphrase"), "-passphrase", "test", "-o", out}) + ok(t, err) + + rawPrivateKey, err := ioutil.ReadFile(out) + privateKey := strings.TrimSuffix(string(rawPrivateKey), "\n") + ok(t, err) + + fmt.Printf("private key: %s\n", privateKey) + _, err = age.ParseX25519Identity(privateKey) + ok(t, err) +} diff --git a/cmd/ssh-to-age/test-assets/id_ed25519_passphrase b/cmd/ssh-to-age/test-assets/id_ed25519_passphrase new file mode 100644 index 0000000..9d08ed2 --- /dev/null +++ b/cmd/ssh-to-age/test-assets/id_ed25519_passphrase @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABC83l/B2p +MGlU+7xBT7wzeuAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAID/+AcTjBdIG6Xwk +4ZiuckvG5xNDlaqX316bKGo6D3a5AAAAkJA09klC9kTXa4VO1n4p3/J0ugw89MNS4eUn2b +4vbCPGrqZGZBU/Byu4A5g/Z03sGxGJj0GqnkC6I8aS2aTeQriNpdm10NaPVRL9dtL0//rp +NT/WAPFTUavHyBT16tmKKabyKHHf83QdtpbjckXkk8q1Xf8tBKYooZJcieo+22mrmq1Hha +JxU9TKx2Tc2RMymQ== +-----END OPENSSH PRIVATE KEY----- diff --git a/cmd/ssh-to-age/test-assets/id_ed25519_passphrase.pub b/cmd/ssh-to-age/test-assets/id_ed25519_passphrase.pub new file mode 100644 index 0000000..faa863c --- /dev/null +++ b/cmd/ssh-to-age/test-assets/id_ed25519_passphrase.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID/+AcTjBdIG6Xwk4ZiuckvG5xNDlaqX316bKGo6D3a5 diff --git a/convert.go b/convert.go index 9624754..92478fd 100644 --- a/convert.go +++ b/convert.go @@ -1,9 +1,9 @@ package agessh import ( + "crypto" "crypto/ed25519" "crypto/sha512" - "crypto" "errors" "fmt" "reflect" @@ -57,8 +57,16 @@ func encodePublicKey(key crypto.PublicKey) (*string, error) { return &s, nil } -func SSHPrivateKeyToAge(sshKey []byte) (*string, *string, error) { - privateKey, err := ssh.ParseRawPrivateKey(sshKey) +func SSHPrivateKeyToAge(sshKey, passphrase []byte) (*string, *string, error) { + var ( + privateKey interface{} + err error + ) + if len(passphrase) > 0 { + privateKey, err = ssh.ParseRawPrivateKeyWithPassphrase(sshKey, passphrase) + } else { + privateKey, err = ssh.ParseRawPrivateKey(sshKey) + } if err != nil { return nil, nil, fmt.Errorf("failed to parse ssh private key: %w", err) } @@ -85,7 +93,6 @@ func SSHPrivateKeyToAge(sshKey []byte) (*string, *string, error) { return &s, pubKey, nil } - func SSHPublicKeyToAge(sshKey []byte) (*string, error) { var err error var pk ssh.PublicKey From aa1800340e9d450dfcf0b669205c94d76d41d838 Mon Sep 17 00:00:00 2001 From: Samuel Rounce Date: Thu, 22 Dec 2022 11:08:03 +0000 Subject: [PATCH 2/5] Use env var to pass key passphrase instead of `passphrase` flag --- cmd/ssh-to-age/main.go | 11 +++++++---- cmd/ssh-to-age/main_test.go | 7 ++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/cmd/ssh-to-age/main.go b/cmd/ssh-to-age/main.go index 0dfbac9..c7846d0 100644 --- a/cmd/ssh-to-age/main.go +++ b/cmd/ssh-to-age/main.go @@ -12,8 +12,8 @@ import ( ) type options struct { - out, in, passphrase string - privateKey bool + out, in string + privateKey, passphrase bool } func parseFlags(args []string) options { @@ -22,7 +22,7 @@ func parseFlags(args []string) options { f.BoolVar(&opts.privateKey, "private-key", false, "convert private key instead of public key") f.StringVar(&opts.in, "i", "-", "Input path. Reads by default from standard output") f.StringVar(&opts.out, "o", "-", "Output path. Prints by default to standard output") - f.StringVar(&opts.passphrase, "passphrase", "", "Output path. Prints by default to standard output") + f.BoolVar(&opts.passphrase, "passphrase", false, "Indicate private key passphrase should be read from `SSH_TO_AGE_PASSPHRASE` environment variable") if err := f.Parse(args[1:]); err != nil { // should never happen since flag.ExitOnError panic(err) @@ -69,7 +69,10 @@ func convertKeys(args []string) error { key *string err error ) - key, _, err = sshage.SSHPrivateKeyToAge(sshKey, []byte(opts.passphrase)) + + keyPassphrase := os.Getenv("SSH_TO_AGE_PASSPHRASE") + + key, _, err = sshage.SSHPrivateKeyToAge(sshKey, []byte(keyPassphrase)) if err != nil { return fmt.Errorf("failed to convert '%s': %w", sshKey, err) } diff --git a/cmd/ssh-to-age/main_test.go b/cmd/ssh-to-age/main_test.go index f6c2d3a..b80e11e 100644 --- a/cmd/ssh-to-age/main_test.go +++ b/cmd/ssh-to-age/main_test.go @@ -98,7 +98,12 @@ func TestPrivateKeyWithPassphrase(t *testing.T) { defer os.RemoveAll(tempdir) out := path.Join(tempdir, "out") - err := convertKeys([]string{"ssh-to-age", "-private-key", "-i", Asset("id_ed25519_passphrase"), "-passphrase", "test", "-o", out}) + passphrase := "test" + + os.Setenv("SSH_TO_AGE_PASSPHRASE", passphrase) + defer os.Unsetenv("SSH_TO_AGE_PASSPHRASE") + + err := convertKeys([]string{"ssh-to-age", "-private-key", "-i", Asset("id_ed25519_passphrase"), "-passphrase", "-o", out}) ok(t, err) rawPrivateKey, err := ioutil.ReadFile(out) From 9ce65f9c370767d05c072e187df5aa82ea0a66d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 23 Dec 2022 10:58:06 +0100 Subject: [PATCH 3/5] drop passphrase flag --- cmd/ssh-to-age/main.go | 8 ++++---- cmd/ssh-to-age/main_test.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/ssh-to-age/main.go b/cmd/ssh-to-age/main.go index c7846d0..b034ee9 100644 --- a/cmd/ssh-to-age/main.go +++ b/cmd/ssh-to-age/main.go @@ -4,16 +4,17 @@ import ( "errors" "flag" "fmt" - sshage "github.com/Mic92/ssh-to-age" "io" "io/ioutil" "os" "strings" + + sshage "github.com/Mic92/ssh-to-age" ) type options struct { - out, in string - privateKey, passphrase bool + out, in string + privateKey bool } func parseFlags(args []string) options { @@ -22,7 +23,6 @@ func parseFlags(args []string) options { f.BoolVar(&opts.privateKey, "private-key", false, "convert private key instead of public key") f.StringVar(&opts.in, "i", "-", "Input path. Reads by default from standard output") f.StringVar(&opts.out, "o", "-", "Output path. Prints by default to standard output") - f.BoolVar(&opts.passphrase, "passphrase", false, "Indicate private key passphrase should be read from `SSH_TO_AGE_PASSPHRASE` environment variable") if err := f.Parse(args[1:]); err != nil { // should never happen since flag.ExitOnError panic(err) diff --git a/cmd/ssh-to-age/main_test.go b/cmd/ssh-to-age/main_test.go index b80e11e..de83d6e 100644 --- a/cmd/ssh-to-age/main_test.go +++ b/cmd/ssh-to-age/main_test.go @@ -103,7 +103,7 @@ func TestPrivateKeyWithPassphrase(t *testing.T) { os.Setenv("SSH_TO_AGE_PASSPHRASE", passphrase) defer os.Unsetenv("SSH_TO_AGE_PASSPHRASE") - err := convertKeys([]string{"ssh-to-age", "-private-key", "-i", Asset("id_ed25519_passphrase"), "-passphrase", "-o", out}) + err := convertKeys([]string{"ssh-to-age", "-private-key", "-i", Asset("id_ed25519_passphrase"), "-o", out}) ok(t, err) rawPrivateKey, err := ioutil.ReadFile(out) From 3905acecf3cbbebfc546fed332476b325de9364f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 23 Dec 2022 11:10:13 +0100 Subject: [PATCH 4/5] add passphrase example to readme --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 0407f86..ca43f2d 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,13 @@ $ cat key.txt AGE-SECRET-KEY-1K3VN4N03PTHJWSJSCCMQCN33RY5FSKQPJ4KRRTG3JMQUYE0TUSEQEDH6V8 ``` +If you private key is encrypted, you can export the password in `SSH_TO_AGE_PASSPHRASE` + +``` console +$ read -s SSH_TO_AGE_PASSPHRASE; export SSH_TO_AGE_PASSPHRASE +$ ssh-to-age -private-key -i $HOME/.ssh/id_ed25519 -o key.txt +``` + - Exports the public key: ```console From 6e73e98112f71b3c4df514f685f869eb09c0108e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 23 Dec 2022 11:11:59 +0100 Subject: [PATCH 5/5] bump nixos to 22.11 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b31bf8d..3d61e56 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: nixPath: - - nixpkgs=https://github.com/NixOS/nixpkgs/archive/nixos-22.05.tar.gz + - nixpkgs=https://github.com/NixOS/nixpkgs/archive/nixos-22.11.tar.gz - nixpkgs=https://github.com/NixOS/nixpkgs/archive/nixpkgs-unstable.tar.gz os: [ ubuntu-latest, macos-latest ] runs-on: ${{ matrix.os }}