Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 47 additions & 29 deletions cmd/minisign/minisign.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
)

const usage = `Usage:
minisign -G [-p <pubKey>] [-s <secKey>]
minisign -G [-p <pubKey>] [-s <secKey>] [-W]
minisign -S [-x <signature>] [-s <secKey>] [-c <comment>] [-t <comment>] -m <file>...
minisign -V [-H] [-x <signature>] [-p <pubKey> | -P <pubKey>] [-o] [-q | -Q ] -m <file>
minisign -R [-s <secKey>] [-p <pubKey>]
Expand All @@ -38,6 +38,7 @@ Options:
-p <pubKey> Public key file (default: ./minisign.pub)
-P <pubKey> Public key as base64 string
-s <secKey> Secret key file (default: $HOME/.minisign/minisign.key)
-W Do not encrypt/decrypt the secret key with a password.
-x <signature> Signature file (default: <file>.minisig)
-c <comment> Add a one-line untrusted comment.
-t <comment> Add a one-line trusted comment.
Expand Down Expand Up @@ -66,6 +67,7 @@ func main() {
pubKeyFileFlag string
pubKeyFlag string
secKeyFileFlag string
unencryptedKeyFlag bool
signatureFlag string
untrustedCommentFlag string
trustedCommentFlag string
Expand All @@ -84,6 +86,7 @@ func main() {
flag.StringVar(&pubKeyFileFlag, "p", "minisign.pub", "Public key file (default: minisign.pub")
flag.StringVar(&pubKeyFlag, "P", "", "Public key as base64 string")
flag.StringVar(&secKeyFileFlag, "s", filepath.Join(os.Getenv("HOME"), ".minisign/minisign.key"), "Secret key file (default: $HOME/.minisign/minisign.key")
flag.BoolVar(&unencryptedKeyFlag, "W", false, "Do not encrypt/decrypt the secret key with a password")
flag.StringVar(&signatureFlag, "x", "", "Signature file (default: <file>.minisig)")
flag.StringVar(&untrustedCommentFlag, "c", "", "Add a one-line untrusted comment")
flag.StringVar(&trustedCommentFlag, "t", "", "Add a one-line trusted comment")
Expand All @@ -102,7 +105,7 @@ func main() {

switch {
case keyGenFlag:
generateKeyPair(secKeyFileFlag, pubKeyFileFlag, forceFlag)
generateKeyPair(secKeyFileFlag, pubKeyFileFlag, forceFlag, unencryptedKeyFlag)
case signFlag:
signFiles(secKeyFileFlag, signatureFlag, untrustedCommentFlag, trustedCommentFlag, filesFlag...)
case verifyFlag:
Expand All @@ -115,7 +118,7 @@ func main() {
}
}

func generateKeyPair(secKeyFile, pubKeyFile string, force bool) {
func generateKeyPair(secKeyFile, pubKeyFile string, force, unencrypted bool) {
if !force {
_, err := os.Stat(secKeyFile)
if err == nil {
Expand Down Expand Up @@ -145,29 +148,38 @@ func generateKeyPair(secKeyFile, pubKeyFile string, force bool) {
}
}

var password string
if term.IsTerminal(int(os.Stdin.Fd())) {
fmt.Print("Please enter a password to protect the secret key.\n\n")
password = readPassword(os.Stdin, "Enter Password: ")
passwordAgain := readPassword(os.Stdin, "Enter Password (one more time): ")
if password != passwordAgain {
log.Fatal("Error: passwords don't match")
}
} else {
password = readPassword(os.Stdin, "Enter Password: ")
}
publicKey, privateKey, err := minisign.GenerateKey(rand.Reader)
if err != nil {
log.Fatalf("Error: %v", err)
}

fmt.Print("Deriving a key from the password in order to encrypt the secret key... ")
encryptedPrivateKey, err := minisign.EncryptKey(password, privateKey)
if err != nil {
fmt.Println()
log.Fatalf("Error: %v", err)
var privateKeyBytes []byte
if unencrypted {
privateKeyBytes, err = privateKey.MarshalText()
if err != nil {
log.Fatalf("Error: %v", err)
}
} else {
var password string
if term.IsTerminal(int(os.Stdin.Fd())) {
fmt.Print("Please enter a password to protect the secret key.\n\n")
password = readPassword(os.Stdin, "Enter Password: ")
passwordAgain := readPassword(os.Stdin, "Enter Password (one more time): ")
if password != passwordAgain {
log.Fatal("Error: passwords don't match")
}
} else {
password = readPassword(os.Stdin, "Enter Password: ")
}

fmt.Print("Deriving a key from the password in order to encrypt the secret key... ")
privateKeyBytes, err = minisign.EncryptKey(password, privateKey)
if err != nil {
fmt.Println()
log.Fatalf("Error: %v", err)
}
fmt.Print("done\n\n")
}
fmt.Print("done\n\n")

fileFlags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC
if !force {
Expand All @@ -178,7 +190,7 @@ func generateKeyPair(secKeyFile, pubKeyFile string, force bool) {
log.Fatalf("Error: %v", err)
}
defer skFile.Close()
if _, err = skFile.Write(encryptedPrivateKey); err != nil {
if _, err = skFile.Write(privateKeyBytes); err != nil {
log.Fatalf("Error: %v", err)
}

Expand Down Expand Up @@ -218,19 +230,25 @@ func signFiles(secKeyFile, sigFile, untrustedComment, trustedComment string, fil
}
}

encryptedPrivateKey, err := os.ReadFile(secKeyFile)
privateKeyBytes, err := os.ReadFile(secKeyFile)
if err != nil {
log.Fatalf("Error: %v", err)
}
password := readPassword(os.Stdin, "Enter password: ")

fmt.Print("Deriving a key from the password in order to decrypt the secret key... ")
privateKey, err := minisign.DecryptKey(password, encryptedPrivateKey)
if err != nil {
fmt.Println()
log.Fatalf("Error: invalid password: %v", err)
var privateKey minisign.PrivateKey
if minisign.IsEncrypted(privateKeyBytes) {
password := readPassword(os.Stdin, "Enter password: ")

fmt.Print("Deriving a key from the password in order to decrypt the secret key... ")
privateKey, err = minisign.DecryptKey(password, privateKeyBytes)
if err != nil {
fmt.Println()
log.Fatalf("Error: invalid password: %v", err)
}
fmt.Print("done\n\n")
} else if err = privateKey.UnmarshalText(privateKeyBytes); err != nil {
log.Fatalf("Error: %v", err)
}
fmt.Print("done\n\n")

if sigFile != "" {
if dir := filepath.Dir(sigFile); dir != "" && dir != "." && dir != "/" {
Expand Down
5 changes: 2 additions & 3 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"crypto/rand"
"fmt"
"io"
"io/ioutil"
"strconv"
"strings"

Expand Down Expand Up @@ -160,15 +159,15 @@ func ExampleReader() {

// Sign a data stream after processing it. (Here, we just discard it)
reader := minisign.NewReader(strings.NewReader(Message))
if _, err := io.Copy(ioutil.Discard, reader); err != nil {
if _, err := io.Copy(io.Discard, reader); err != nil {
panic(err) // TODO: error handling
}
signature := reader.Sign(privateKey)

// Read a data stream and then verify its authenticity with
// the public key.
reader = minisign.NewReader(strings.NewReader(Message))
message, err := ioutil.ReadAll(reader)
message, err := io.ReadAll(reader)
if err != nil {
panic(err) // TODO: error handling
}
Expand Down
2 changes: 2 additions & 0 deletions internal/testdata/minisign_unencrypted.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
untrusted comment: minisign encrypted secret key
RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbuUYgQpHKDcmmMQj9cgqohWX321PrXUDFfCVWOXDZp8kLw2/qju66KnI28LcOaA7ZywNP5vDVtlHeyzit3lxeqirS5+2UImrAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
5 changes: 2 additions & 3 deletions minisign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package minisign

import (
"io"
"io/ioutil"
"os"
"testing"
)
Expand All @@ -18,7 +17,7 @@ func TestRoundtrip(t *testing.T) {
t.Fatalf("Failed to load private key: %v", err)
}

message, err := ioutil.ReadFile("./internal/testdata/message.txt")
message, err := os.ReadFile("./internal/testdata/message.txt")
if err != nil {
t.Fatalf("Failed to load message: %v", err)
}
Expand Down Expand Up @@ -48,7 +47,7 @@ func TestReaderRoundtrip(t *testing.T) {
defer file.Close()

reader := NewReader(file)
if _, err = io.Copy(ioutil.Discard, reader); err != nil {
if _, err = io.Copy(io.Discard, reader); err != nil {
t.Fatalf("Failed to read message: %v", err)
}
signature := reader.Sign(privateKey)
Expand Down
117 changes: 107 additions & 10 deletions private.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@
package minisign

import (
"bytes"
"crypto"
"crypto/ed25519"
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"strconv"
"strings"
"time"
Expand All @@ -25,7 +27,7 @@ import (
// PrivateKeyFromFile reads and decrypts the private key
// file with the given password.
func PrivateKeyFromFile(password, path string) (PrivateKey, error) {
bytes, err := ioutil.ReadFile(path)
bytes, err := os.ReadFile(path)
if err != nil {
return PrivateKey{}, err
}
Expand All @@ -34,8 +36,7 @@ func PrivateKeyFromFile(password, path string) (PrivateKey, error) {

// PrivateKey is a minisign private key.
//
// A private key can sign messages to prove the
// their origin and authenticity.
// A private key can sign messages to prove their origin and authenticity.
//
// PrivateKey implements the crypto.Signer interface.
type PrivateKey struct {
Expand Down Expand Up @@ -101,9 +102,89 @@ func (p PrivateKey) Equal(x crypto.PrivateKey) bool {
return p.id == xx.id && subtle.ConstantTimeCompare(p.bytes[:], xx.bytes[:]) == 1
}

// MarshalText returns a textual representation of the private key.
//
// For password-protected private keys refer to [EncryptKey].
func (p PrivateKey) MarshalText() ([]byte, error) {
var b [privateKeySize]byte

binary.LittleEndian.PutUint16(b[:], EdDSA)
binary.LittleEndian.PutUint16(b[2:], algorithmNone)
binary.LittleEndian.PutUint16(b[4:], algorithmBlake2b)

binary.LittleEndian.PutUint64(b[54:], p.id)
copy(b[62:], p.bytes[:])

const comment = "untrusted comment: minisign encrypted secret key\n"
encodedBytes := make([]byte, len(comment)+base64.StdEncoding.EncodedLen(len(b)))
copy(encodedBytes, []byte(comment))
base64.StdEncoding.Encode(encodedBytes[len(comment):], b[:])
return encodedBytes, nil
}

// UnmarshalText decodes a textual representation of the private key into p.
//
// It returns an error if the private key is encrypted. For decrypting
// password-protected private keys refer to [DecryptKey].
func (p *PrivateKey) UnmarshalText(text []byte) error {
text = trimUntrustedComment(text)
b := make([]byte, base64.StdEncoding.DecodedLen(len(text)))
n, err := base64.StdEncoding.Decode(b, text)
if err != nil {
return fmt.Errorf("minisign: invalid private key: %v", err)
}
b = b[:n]

if len(b) != privateKeySize {
return errors.New("minisign: invalid private key")
}

var (
empty [32]byte

kType = binary.LittleEndian.Uint16(b)
kdf = binary.LittleEndian.Uint16(b[2:])
hType = binary.LittleEndian.Uint16(b[4:])
salt = b[6:38]
scryptOps = binary.LittleEndian.Uint64(b[38:])
scryptMem = binary.LittleEndian.Uint64(b[46:])
key = b[54:126]
checksum = b[126:privateKeySize]
)
if kType != EdDSA {
return fmt.Errorf("minisign: invalid private key: invalid key type '%d'", kType)
}
if kdf == algorithmScrypt {
return errors.New("minisign: private key is encrypted")
}
if kdf != algorithmNone {
return fmt.Errorf("minisign: invalid private key: invalid KDF '%d'", kdf)
}
if hType != algorithmBlake2b {
return fmt.Errorf("minisign: invalid private key: invalid hash type '%d'", hType)
}
if !bytes.Equal(salt[:], empty[:]) {
return errors.New("minisign: invalid private key: salt is not empty")
}
if scryptOps != 0 {
return errors.New("minisign: invalid private key: scrypt cost parameter is not zero")
}
if scryptMem != 0 {
return errors.New("minisign: invalid private key: scrypt mem parameter is not zero")
}
if !bytes.Equal(checksum, empty[:]) {
return errors.New("minisign: invalid private key: salt is not empty")
}

p.id = binary.LittleEndian.Uint64(key[:8])
copy(p.bytes[:], key[8:])
return nil
}

const (
scryptAlgorithm = 0x6353 // hex value for "Sc"
blake2bAlgorithm = 0x3242 // hex value for "B2"
algorithmNone = 0x0000 // hex value for KDF when key is not encrypted
algorithmScrypt = 0x6353 // hex value for "Sc"
algorithmBlake2b = 0x3242 // hex value for "B2"

scryptOpsLimit = 0x2000000 // max. Scrypt ops limit based on libsodium
scryptMemLimit = 0x40000000 // max. Scrypt mem limit based on libsodium
Expand All @@ -125,8 +206,8 @@ func EncryptKey(password string, privateKey PrivateKey) ([]byte, error) {

var bytes [privateKeySize]byte
binary.LittleEndian.PutUint16(bytes[0:], EdDSA)
binary.LittleEndian.PutUint16(bytes[2:], scryptAlgorithm)
binary.LittleEndian.PutUint16(bytes[4:], blake2bAlgorithm)
binary.LittleEndian.PutUint16(bytes[2:], algorithmScrypt)
binary.LittleEndian.PutUint16(bytes[4:], algorithmBlake2b)

const ( // TODO(aead): Callers may want to customize the cost parameters
defaultOps = 33554432 // libsodium OPS_LIMIT_SENSITIVE
Expand All @@ -144,6 +225,22 @@ func EncryptKey(password string, privateKey PrivateKey) ([]byte, error) {
return encodedBytes, nil
}

// IsEncrypted reports whether the private key is encrypted.
func IsEncrypted(privateKey []byte) bool {
privateKey = trimUntrustedComment(privateKey)
bytes := make([]byte, base64.StdEncoding.DecodedLen(len(privateKey)))
n, err := base64.StdEncoding.Decode(bytes, privateKey)
if err != nil {
return false
}
bytes = bytes[:n]

if len(bytes) != privateKeySize {
return false
}
return binary.LittleEndian.Uint16(bytes[2:4]) == algorithmScrypt
}

var errDecrypt = errors.New("minisign: decryption failed")

// DecryptKey tries to decrypt the encrypted private key with
Expand All @@ -163,10 +260,10 @@ func DecryptKey(password string, privateKey []byte) (PrivateKey, error) {
if a := binary.LittleEndian.Uint16(bytes[:2]); a != EdDSA {
return PrivateKey{}, errDecrypt
}
if a := binary.LittleEndian.Uint16(bytes[2:4]); a != scryptAlgorithm {
if a := binary.LittleEndian.Uint16(bytes[2:4]); a != algorithmScrypt {
return PrivateKey{}, errDecrypt
}
if a := binary.LittleEndian.Uint16(bytes[4:6]); a != blake2bAlgorithm {
if a := binary.LittleEndian.Uint16(bytes[4:6]); a != algorithmBlake2b {
return PrivateKey{}, errDecrypt
}

Expand Down
Loading