Add support for passphrase protected private keys (#12)

Closes #11

Co-authored-by: James Mills <1290234+prologic@users.noreply.github.com>
Reviewed-on: #12
main
James Mills 1 week ago
parent ce2b9f6573
commit b5dab41cf9
  1. 35
      cmd/salty-keygen/main.go
  2. 30
      cmd/salty/main.go
  3. 2
      crypto_test.go
  4. 2
      doc.go
  5. 45
      keys.go
  6. 33
      keys_test.go

@ -1,9 +1,13 @@
// Package main is the salty-keygen command-line (cli) tool
package main
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"syscall"
log "github.com/sirupsen/logrus"
flag "github.com/spf13/pflag"
@ -29,6 +33,33 @@ func init() {
flag.StringVarP(&output, "output", "o", "", "Write the result to the file")
}
func promptForPassphrase() string {
for {
fmt.Print("Enter passphrase (empty for no passphrase): ")
bytePassword1, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return ""
}
fmt.Println()
fmt.Printf("Enter same passphrase again: ")
bytePassword2, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return ""
}
fmt.Println()
if !bytes.Equal(bytePassword1, bytePassword2) {
fmt.Println("Passphrases do not match. Try again.")
continue
}
fmt.Println()
return strings.TrimSpace(string(bytePassword1))
}
}
func main() {
flag.Parse()
@ -55,7 +86,9 @@ func main() {
log.Warn("writing secret key to a world-readable file")
}
_, pub := salty.GenerateKeys(out)
pwd := promptForPassphrase()
_, pub := salty.GenerateKeys(pwd, out)
if !term.IsTerminal(int(out.Fd())) {
fmt.Fprintf(os.Stderr, "Public key: %s\n", pub)
}

@ -1,15 +1,20 @@
// Package main is the salty command-line (cli) tool
package main
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"syscall"
"github.com/keys-pub/keys"
log "github.com/sirupsen/logrus"
flag "github.com/spf13/pflag"
"go.mills.io/salty"
"golang.org/x/term"
)
var (
@ -41,6 +46,18 @@ func init() {
flag.StringVarP(&identity, "identity", "i", "", "Use the identity file at PATH. Can be repeated.")
}
func promptForPassword() string {
fmt.Print("Enter password for private key: ")
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return ""
}
fmt.Println()
return strings.TrimSpace(string(bytePassword))
}
func main() {
flag.Parse()
@ -70,9 +87,18 @@ func main() {
}
defer id.Close()
key, err = salty.ParseIdentity(id)
key, err = salty.ParseIdentity("", id)
if err != nil {
log.WithError(err).Fatalf("error reading private key: %q", identity)
if errors.Is(err, salty.ErrProtectedKey) {
id.Seek(0, os.SEEK_SET)
pwd := promptForPassword()
key, err = salty.ParseIdentity(pwd, id)
if err != nil {
log.WithError(err).Fatalf("error reading private key: %q", identity)
}
} else {
log.WithError(err).Fatalf("error reading private key: %q", identity)
}
}
}

@ -10,7 +10,7 @@ import (
func TestCrypto(t *testing.T) {
assert := assert.New(t)
key, pub := GenerateKeys(io.Discard)
key, pub := GenerateKeys("", io.Discard)
assert.NotEmpty(key)
assert.NotEmpty(pub)
assert.Equal(key.PublicKey().String(), pub)

@ -0,0 +1,2 @@
// Package salty is a library and set of command-line (cli) tools for working with ED25519 keys and th saltpack message format
package salty

@ -2,7 +2,9 @@ package salty
import (
"bufio"
"crypto/ed25519"
"encoding/base64"
"errors"
"fmt"
"io"
"strings"
@ -11,17 +13,28 @@ import (
"github.com/keys-pub/keys"
)
const privateKeySizeLimit = 1 << 8 // 256 bytes
var (
// ErrProtectedKey is an error returned when a private key is protected by a password, but none was provided.
ErrProtectedKey = errors.New("error: key projtected by a password")
)
// GenerateKeys creates a new pair of Ed25519 keys and writes the Private Key
// to the `out io.Writer` and returns the Private and Public Keys.
// The Private Key written to `out` is Base64 encoded.
func GenerateKeys(out io.Writer) (*keys.EdX25519Key, string) {
func GenerateKeys(pwd string, out io.Writer) (*keys.EdX25519Key, string) {
k := keys.GenerateEdX25519Key()
var encodedKey string
if pwd != "" {
encodedKey = base64.StdEncoding.EncodeToString(keys.EncryptWithPassword(k.Private(), pwd))
} else {
encodedKey = base64.StdEncoding.EncodeToString(k.Private())
}
fmt.Fprintf(out, "# created: %s\n", time.Now().Format(time.RFC3339))
fmt.Fprintf(out, "# public key: %s\n", k.PublicKey().ID().String())
fmt.Fprintf(out, "%s\n", base64.StdEncoding.EncodeToString(k.Private()))
fmt.Fprintf(out, "%s\n", encodedKey)
return k, k.PublicKey().ID().String()
}
@ -31,8 +44,8 @@ func GenerateKeys(out io.Writer) (*keys.EdX25519Key, string) {
// lines are ignored and the private key is the first non-comment / non-blank line.
// The Private Key is a Base64 decoded.
// This returns the parsed Ed25519 key on success or nil key and error if it fails.
func ParseIdentity(r io.Reader) (*keys.EdX25519Key, error) {
scanner := bufio.NewScanner(io.LimitReader(r, privateKeySizeLimit))
func ParseIdentity(pwd string, r io.Reader) (*keys.EdX25519Key, error) {
scanner := bufio.NewScanner(r)
var n int
for scanner.Scan() {
line := scanner.Text()
@ -40,11 +53,27 @@ func ParseIdentity(r io.Reader) (*keys.EdX25519Key, error) {
if strings.HasPrefix(line, "#") || line == "" {
continue
}
bs, err := base64.StdEncoding.DecodeString(line)
decodedKey, err := base64.StdEncoding.DecodeString(line)
if err != nil {
return nil, fmt.Errorf("error at line %d: %v", n, err)
return nil, fmt.Errorf("error decoding key: %w", err)
}
return keys.NewEdX25519KeyFromPrivateKey(keys.Bytes64(bs)), nil
var decryptedKey []byte
if len(decodedKey) > ed25519.PrivateKeySize {
if pwd == "" {
return nil, ErrProtectedKey
}
decryptedKey, err = keys.DecryptWithPassword(decodedKey, pwd)
if err != nil {
return nil, fmt.Errorf("error decrypting key: %w", err)
}
} else {
decryptedKey = decodedKey[:]
}
return keys.NewEdX25519KeyFromPrivateKey(keys.Bytes64(decryptedKey)), nil
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("failed to read identity file: %v", err)

@ -5,17 +5,36 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestKeys(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
buf := &bytes.Buffer{}
key, pub := GenerateKeys(buf)
assert.NotEmpty(pub)
t.Run("WithPassword", func(t *testing.T) {
buf := &bytes.Buffer{}
pwd := "password123"
key, pub := GenerateKeys(pwd, buf)
assert.NotEmpty(pub)
parsedKey, err := ParseIdentity(pwd, buf)
require.NoError(err)
assert.Equal(key, parsedKey)
assert.Equal(pub, key.PublicKey().String())
})
t.Run("WithoutPassword", func(t *testing.T) {
buf := &bytes.Buffer{}
key, pub := GenerateKeys("", buf)
assert.NotEmpty(pub)
parsedKey, err := ParseIdentity("", buf)
require.NoError(err)
assert.Equal(key, parsedKey)
assert.Equal(pub, key.PublicKey().String())
})
parsedKey, err := ParseIdentity(buf)
assert.NoError(err)
assert.Equal(key, parsedKey)
assert.Equal(pub, key.PublicKey().String())
}

Loading…
Cancel
Save