diff --git a/cipher.go b/cipher.go new file mode 100644 index 0000000..4853701 --- /dev/null +++ b/cipher.go @@ -0,0 +1,73 @@ +package ecc + +import ( + "crypto/aes" + "crypto/cipher" + "encoding/asn1" + "strings" + "github.com/pkg/errors" + "golang.org/x/crypto/chacha20poly1305" +) + +type Cipher struct { + Name string + Description string + OID asn1.ObjectIdentifier + KeySize int + New func(key []byte) (cipher.AEAD, error) + KDF *KDF +} + +// Suppported ciphers +var Ciphers = []*Cipher{ + { + Name: "aes-128-gcm", + OID: asn1.ObjectIdentifier{2,16,840,1,101,3,4,1,6}, + KeySize: 16, + New: newAESGCM, + KDF: KDFs["hkdf-sha256"], + }, + { + Name: "aes-192-gcm", + OID: asn1.ObjectIdentifier{2,16,840,1,101,3,4,1,26}, + KeySize: 24, + New: newAESGCM, + KDF: KDFs["hkdf-sha256"], + }, + { + Name: "aes-256-gcm", + OID: asn1.ObjectIdentifier{2,16,840,1,101,3,4,1,46}, + KeySize: 32, + New: newAESGCM, + KDF: KDFs["hkdf-sha384"], + }, + { + Name: "chacha20-poly1305", + OID: asn1.ObjectIdentifier{1,2,840,113549,1,9,16,3,18}, + KeySize: 32, + New: chacha20poly1305.New, + KDF: KDFs["hkdf-sha384"], + }, +} + +func LookupCipher(name string) *Cipher { + name = strings.ReplaceAll(strings.ReplaceAll(strings.ToLower(name), "-", ""), "_", "") + for _, c := range Ciphers { + if strings.ReplaceAll(c.Name, "-", "") == name { + return c + } + } + return nil +} + +func newAESGCM(key []byte) (cipher.AEAD, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, errors.Wrapf(err, "init AES") + } + aead, err := cipher.NewGCM(block) + if err != nil { + return nil, errors.Wrapf(err, "init GCM") + } + return aead, nil +} diff --git a/cmd/ecc/.gitignore b/cmd/ecc/.gitignore new file mode 100644 index 0000000..bb1358a --- /dev/null +++ b/cmd/ecc/.gitignore @@ -0,0 +1,3 @@ +dist/ +ecc +test.* diff --git a/cmd/ecc/.goreleaser.yaml b/cmd/ecc/.goreleaser.yaml new file mode 100644 index 0000000..228f884 --- /dev/null +++ b/cmd/ecc/.goreleaser.yaml @@ -0,0 +1,27 @@ +before: + hooks: + - go mod tidy +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin +archives: + - replacements: + darwin: Darwin + linux: Linux + windows: Windows + 386: i386 + amd64: x86_64 +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ incpatch .Version }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' diff --git a/cmd/ecc/commands/decrypt.go b/cmd/ecc/commands/decrypt.go new file mode 100644 index 0000000..fd2bf32 --- /dev/null +++ b/cmd/ecc/commands/decrypt.go @@ -0,0 +1,96 @@ +package commands + +import ( + "io" + "os" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/thepax/ecc" + "github.com/thepax/ecc/eccutil" +) + +type DecryptOpts struct { + Key string + Verify string + In string + Out string +} + +var decryptOpts DecryptOpts + +func DecryptCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "decrypt", + Short: "Decrypt file or stream", + Args: cobra.ExactArgs(0), + PreRunE: func(cmd *cobra.Command, args []string) error { + if decryptOpts.Key == "" { + return errors.New("private key is not specified") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + decryptRun(&decryptOpts) + }, + } + cmd.Flags().StringVar(&decryptOpts.Key, "key", eccutil.GetenvDecryptKey(), "decrypt/private key") + cmd.Flags().StringVar(&decryptOpts.Verify, "verify", eccutil.GetenvVerifyKey(), "public key") + cmd.Flags().StringVarP(&decryptOpts.In, "in", "i", "", "input file (default is stdin)") + cmd.Flags().StringVarP(&decryptOpts.Out, "out", "o", "", "output file (default is stdout)") + return cmd +} + +func decryptRun(opts *DecryptOpts) { + var err error + + decryptKey, err := eccutil.GetPrivateKey("decrypting", opts.Key) + if err != nil { + log.Fatal(err) + } + verifyKey, err := eccutil.GetPublicKey("verifying", opts.Verify) + if err != nil { + log.Fatal(err) + } + + var in *os.File + if opts.In != "" { + in, err = os.Open(opts.In) + if err != nil { + log.Fatal("open input: ", err) + } + defer in.Close() + } else { + in = os.Stdin + } + var out *os.File + if opts.Out != "" { + out, err = os.OpenFile(opts.Out, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + log.Fatal("create output: ", err) + } + defer func() { + if err == nil { + err = out.Close() + if err != nil { + os.Remove(opts.Out) + log.Fatal("close output: ", err) + } + } else { + out.Close() + os.Remove(opts.Out) + } + }() + } else { + out = os.Stdout + } + dec := &ecc.Decrypt{ + PrivateKey: decryptKey, + VerifyKey: verifyKey, + Input: in, + } + _, err = io.Copy(out, dec) + if err != nil { + log.Fatal("decrypt: ", err) + } +} diff --git a/cmd/ecc/commands/edit.go b/cmd/ecc/commands/edit.go new file mode 100644 index 0000000..09f8946 --- /dev/null +++ b/cmd/ecc/commands/edit.go @@ -0,0 +1,162 @@ +package commands + +import ( + "crypto/rand" + "io" + "os" + "os/exec" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/thepax/ecc" + "github.com/thepax/ecc/eccutil" +) + +type EditOpts struct { + Cipher string + Key string + Rand string +} + +var editOpts EditOpts + +func EditCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "edit [flags] filename.ecc", + Short: "Edit an encrypted file", + Args: cobra.ExactArgs(1), + PreRunE: func(cmd *cobra.Command, args []string) error { + if editOpts.Key == "" { + return errors.New("private key is not specified") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + editRun(&editOpts, args[0]) + }, + } + cmd.Flags().StringVar(&editOpts.Cipher, "cipher", "aes-256-gcm", "encryption cipher") + cmd.Flags().StringVar(&editOpts.Key, "key", eccutil.GetenvPrivateKey(), "private key") + cmd.Flags().StringVar(&editOpts.Rand, "rand", "", "file to use for random number input") + return cmd +} + +func editRun(opts *EditOpts, filename string) { + err := editDoRun(opts, filename) + if err != nil { + log.Fatal(err) + } +} + +func editDoRun(opts *EditOpts, filename string) error { + privateKey, err := eccutil.GetPrivateKey("decrypting", opts.Key) + if err != nil { + return err + } + + randReader := rand.Reader + if opts.Rand != "" { + f, err := os.Open(opts.Rand) + if err != nil { + log.Fatal("open random number input: ", err) + } + randReader = f + defer f.Close() + } + + var f *os.File + if _, err := os.Stat(filename); !os.IsNotExist(err) { + f, err = os.OpenFile(filename, os.O_RDWR, 0600) + if err != nil { + return err + } + defer f.Close() + } + + tmpf, err := os.CreateTemp("", "ecc*.txt") + if err != nil { + return err + } + tempfile := tmpf.Name() + defer func() { + if fi, err := os.Stat(tempfile); err == nil && fi.Size() > 0 { + tmpf, err := os.OpenFile(tempfile, os.O_RDWR, 0600) + if err == nil { + io.CopyN(tmpf, randReader, fi.Size()) + tmpf.Close() + } + } + os.Remove(tempfile) + }() + + if f != nil { + dec := &ecc.Decrypt{ + PrivateKey: privateKey, + VerifyKey: &privateKey.PublicKey, + Input: f, + } + _, err = io.Copy(tmpf, dec) + if err != nil { + tmpf.Close() + return errors.Wrap(err, "decrypt") + } + } + if err = tmpf.Close(); err != nil { + return errors.Wrap(err, "decrypt") + } + + editor := os.Getenv("EDITOR") + if editor == "" { + editor = "vi" + } + cmd := exec.Command(editor, tempfile) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return errors.Wrap(err, "run editor") + } + + if f == nil { + f, err = os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer f.Close() + } + if _, err = f.Seek(0, os.SEEK_SET); err != nil { + return err + } + + tmpf, err = os.Open(tempfile) + if err != nil { + return errors.Wrap(err, "encrypt") + } + defer tmpf.Close() + + enc := &ecc.Encrypt{ + PublicKey: &privateKey.PublicKey, + SignKey: privateKey, + Cipher: opts.Cipher, + Output: f, + Random: randReader, + } + _, err = io.Copy(enc, tmpf) + if err != nil { + return errors.Wrap(err, "encrypt") + } + if err = enc.Close(); err != nil { + return errors.Wrap(err, "encrypt") + } + pos, err := f.Seek(0, os.SEEK_CUR) + if err != nil { + return errors.Wrap(err, "encrypt") + } + if err = f.Truncate(pos); err != nil { + return errors.Wrap(err, "encrypt") + } + if err = f.Sync(); err != nil { + return errors.Wrap(err, "encrypt") + } + return nil +} diff --git a/cmd/ecc/commands/encrypt.go b/cmd/ecc/commands/encrypt.go new file mode 100644 index 0000000..3d414cb --- /dev/null +++ b/cmd/ecc/commands/encrypt.go @@ -0,0 +1,124 @@ +package commands + +import ( + "fmt" + "io" + "os" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/thepax/ecc" + "github.com/thepax/ecc/eccutil" +) + +type EncryptOpts struct { + ListCiphers bool + Key string + Sign string + In string + Out string + Rand string + Cipher string + Chunk uint32 +} + +var encryptOpts EncryptOpts + +func EncryptCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "encrypt", + Short: "Encrypt file or stream", + Args: cobra.ExactArgs(0), + PreRunE: func(cmd *cobra.Command, args []string) error { + if !encryptOpts.ListCiphers && encryptOpts.Key == ""{ + return errors.New("public key is not specified") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + encryptRun(&encryptOpts) + }, + } + cmd.Flags().BoolVar(&encryptOpts.ListCiphers, "list-ciphers", false, "list supported ciphers") + cmd.Flags().StringVar(&encryptOpts.Key, "key", eccutil.GetenvEncryptKey(), "encrypt/public key") + cmd.Flags().StringVar(&encryptOpts.Sign, "sign", eccutil.GetenvSignKey(), "private key") + cmd.Flags().StringVarP(&encryptOpts.In, "in", "i", "", "input file (default is stdin)") + cmd.Flags().StringVarP(&encryptOpts.Out, "out", "o", "", "output file (default is stdout)") + cmd.Flags().StringVar(&genkeyOpts.Rand, "rand", "", "file to use for random number input") + cmd.Flags().StringVar(&encryptOpts.Cipher, "cipher", "aes-256-gcm", "encryption cipher") + cmd.Flags().Uint32Var(&encryptOpts.Chunk, "chunk", ecc.DefaultChunkSize, "encryption chunk size") + return cmd +} + +func encryptRun(opts *EncryptOpts) { + if opts.ListCiphers { + for _, c := range ecc.Ciphers { + fmt.Printf("%s: %s\n", c.Name, c.Description) + } + return + } + + encryptKey, err := eccutil.GetPublicKey("encrypting", opts.Key) + if err != nil { + log.Fatal(err) + } + signKey, err := eccutil.GetPrivateKey("signing", opts.Sign) + if err != nil { + log.Fatal(err) + } + + var in *os.File + if opts.In != "" { + in, err = os.Open(opts.In) + if err != nil { + log.Fatal("open input: ", err) + } + defer in.Close() + } else { + in = os.Stdin + } + var out *os.File + if opts.Out != "" { + out, err = os.OpenFile(opts.Out, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + log.Fatal("create output: ", err) + } + defer func() { + if err == nil { + err = out.Close() + if err != nil { + os.Remove(opts.Out) + log.Fatal("close output: ", err) + } + } else { + out.Close() + os.Remove(opts.Out) + } + }() + } else { + out = os.Stdout + } + enc := &ecc.Encrypt{ + PublicKey: encryptKey, + SignKey: signKey, + Cipher: opts.Cipher, + Output: out, + ChunkSize: opts.Chunk, + } + if opts.Rand != "" { + f, err := os.Open(opts.Rand) + if err != nil { + log.Fatal("open random number input: ", err) + } + enc.Random = f + defer f.Close() + } + _, err = io.Copy(enc, in) + if err != nil { + log.Fatal("encrypt: ", err) + } + err = enc.Close() + if err != nil { + log.Fatal("encrypt: ", err) + } +} diff --git a/cmd/ecc/commands/genkey.go b/cmd/ecc/commands/genkey.go new file mode 100644 index 0000000..a4e0d24 --- /dev/null +++ b/cmd/ecc/commands/genkey.go @@ -0,0 +1,254 @@ +package commands + +import ( + "crypto/ecdsa" + "crypto/rand" + "fmt" + "io" + "os" + "strings" + "github.com/pkg/errors" + "github.com/spf13/cobra" + log "github.com/sirupsen/logrus" + "github.com/thepax/ecc" + "github.com/thepax/ecc/eccutil" +) + +type GenkeyOpts struct { + ListCurves bool + Curve string + Cool bool + FromPassword string + Pair bool + Rand string + SSHAgent string + Out string + OutForm string +} + +var genkeyOpts GenkeyOpts + +func GenkeyCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "genkey", + Short: "Generate new EC key", + Example: ` ecc genkey --curve P-224 + ecc genkey --curve secp521r1 + ecc genkey --rand /dev/random + ecc genkey -o my.key + ecc genkey --password secp521r1:1048576:8:1:SALT`, + Args: cobra.ExactArgs(0), + PreRunE: func(cmd *cobra.Command, args []string) error { + genkeyOpts.OutForm = strings.ToLower(genkeyOpts.OutForm) + switch genkeyOpts.OutForm { + case "pem", "short": + default: + return errors.New("invalid output format") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + genkeyRun(&genkeyOpts) + }, + } + cmd.Flags().BoolVar(&genkeyOpts.ListCurves, "list-curves", false, "list supported curves") + cmd.Flags().StringVar(&genkeyOpts.Curve, "curve", "prime256v1", "curve") + cmd.Flags().BoolVar(&genkeyOpts.Cool, "cool", false, "generate cool key") + cmd.Flags().StringVar(&genkeyOpts.FromPassword, "password", "", "generate key from password with scrypt options") + cmd.Flags().BoolVar(&genkeyOpts.Pair, "pair", false, "generate key pair") + cmd.Flags().StringVar(&genkeyOpts.Rand, "rand", "", "file to use for random number input") + cmd.Flags().StringVar(&genkeyOpts.SSHAgent, "ssh-agent", "", "generate key using ssh-agent") + cmd.Flags().StringVarP(&genkeyOpts.Out, "out", "o", "", "output file (default is stdout)") + cmd.Flags().StringVar(&genkeyOpts.OutForm, "outform", "pem", "output format - pem, short") + return cmd +} + +func genkeyRun(opts *GenkeyOpts) { + var err error + + if opts.ListCurves { + for _, curve := range ecc.Curves { + aliases := strings.Join(curve.Aliases, ", ") + if aliases == "" { + fmt.Printf("%s: %s\n", curve.Name, curve.Description) + } else { + fmt.Printf("%s (%s): %s\n", curve.Name, aliases, curve.Description) + } + } + return + } + + curve := ecc.LookupCurve(opts.Curve) + if curve == nil { + log.Fatalf("curve not found: %s", opts.Curve) + } + + var rnd io.Reader + if opts.Rand != "" { + f, err := os.Open(opts.Rand) + if err != nil { + log.Fatal("open random number input: ", err) + } + rnd = f + defer f.Close() + } else { + rnd = rand.Reader + } + + var scryptOpts *ecc.SCryptOpts + if opts.FromPassword != "" { + scryptOpts, err = eccutil.ParseSCryptOpts(opts.FromPassword) + if err != nil { + log.Fatalf("scrypt options: \"%s\": %v", opts.FromPassword, err) + } + } + + var priv1, priv2 *ecdsa.PrivateKey + if scryptOpts != nil { + var password []byte + if opts.Pair { + password, err = eccutil.NewPassword("Side A Password: ") + } else { + password, err = eccutil.NewPassword("Password: ") + } + if err == nil { + priv1, err = ecc.GenerateKeyFromPassword(password, scryptOpts) + } + } else { + if opts.SSHAgent != "" { + priv1, err = eccutil.SSHAgentKey(opts.SSHAgent) + } else { + if opts.Cool { + priv1, err = ecc.GenerateCoolKey(curve, rnd) + } else { + priv1, err = ecc.GenerateKey(curve, rnd) + } + } + } + if err != nil { + log.Fatal("generate key: ", err) + } + if opts.Pair { + if scryptOpts != nil { + var password []byte + password, err = eccutil.NewPassword("Side B Password: ") + if err == nil { + priv2, err = ecc.GenerateKeyFromPassword(password, scryptOpts) + } + } else { + if opts.Cool { + priv2, err = ecc.GenerateCoolKey(curve, rnd) + } else { + priv2, err = ecc.GenerateKey(curve, rnd) + } + } + if err != nil { + log.Fatal("generate key: ", err) + } + } + + var out *os.File + defer func() { + if err != nil { + log.Fatalf("could not create file: %v", err) + } + }() + if opts.Out == "" { + out = os.Stdout + } else { + if out, err = os.OpenFile(opts.Out, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600); err != nil { + return + } + defer func() { + if err == nil { + err = out.Close() + } + }() + } + if priv2 == nil { + switch opts.OutForm { + case "pem": + err = ecc.WritePEM(priv1, out) + case "short": + var shortKey []byte + if shortKey, err = ecc.MarshalShortKey(priv1); err != nil { + return + } + if _, err = out.Write(shortKey); err != nil { + return + } + _, err = out.Write([]byte("\n")) + } + } else { + if _, err = fmt.Fprintf(out, "Side A:\n"); err != nil { + return + } + switch opts.OutForm { + case "pem": + if err = ecc.WritePEM(priv1, out); err != nil { + return + } + if err = ecc.WritePEM(&priv2.PublicKey, out); err != nil { + return + } + case "short": + var shortKey []byte + if shortKey, err = ecc.MarshalShortKey(priv1); err != nil { + return + } + if _, err = out.Write([]byte("Decrypt/sign key: ")); err != nil { + return + } + if _, err = out.Write(shortKey); err != nil { + return + } + if shortKey, err = ecc.MarshalShortKey(&priv2.PublicKey); err != nil { + return + } + if _, err = out.Write([]byte("\nEncrypt/verify key: ")); err != nil { + return + } + if _, err = out.Write(shortKey); err != nil { + return + } + if _, err = out.Write([]byte("\n")); err != nil { + return + } + } + if _, err = out.Write([]byte("\nSide B:\n")); err != nil { + return + } + switch opts.OutForm { + case "pem": + if err = ecc.WritePEM(priv2, out); err != nil { + return + } + if err = ecc.WritePEM(&priv1.PublicKey, out); err != nil { + return + } + case "short": + var shortKey []byte + if shortKey, err = ecc.MarshalShortKey(priv2); err != nil { + return + } + if _, err = out.Write([]byte("Decrypt/sign key: ")); err != nil { + return + } + if _, err = out.Write(shortKey); err != nil { + return + } + if shortKey, err = ecc.MarshalShortKey(&priv1.PublicKey); err != nil { + return + } + if _, err = out.Write([]byte("\nEncrypt/verify key: ")); err != nil { + return + } + if _, err = out.Write(shortKey); err != nil { + return + } + if _, err = out.Write([]byte("\n")); err != nil { + return + } + } + } +} diff --git a/cmd/ecc/commands/key.go b/cmd/ecc/commands/key.go new file mode 100644 index 0000000..817d8d3 --- /dev/null +++ b/cmd/ecc/commands/key.go @@ -0,0 +1,162 @@ +package commands + +import ( + "bytes" + "crypto/ecdsa" + "io" + "os" + "strings" + "github.com/pkg/errors" + "github.com/spf13/cobra" + log "github.com/sirupsen/logrus" + "github.com/thepax/ecc" +) + +type KeyOpts struct { + In string + Out string + NoOut bool + PubOut bool + InForm string + OutForm string +} + +var keyOpts KeyOpts + +func KeyCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "key", + Short: "EC key operations", + Args: cobra.ExactArgs(0), + PreRunE: func(cmd *cobra.Command, args []string) error { + keyOpts.InForm = strings.ToLower(keyOpts.InForm) + switch keyOpts.InForm { + case "pem", "short": + default: + return errors.New("invalid input format") + } + keyOpts.OutForm = strings.ToLower(keyOpts.OutForm) + switch keyOpts.OutForm { + case "pem", "short": + default: + return errors.New("invalid output format") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + keyRun(&keyOpts) + }, + } + cmd.Flags().StringVarP(&keyOpts.In, "in", "i", "", "input file (default is stdin)") + cmd.Flags().StringVarP(&keyOpts.Out, "out", "o", "", "output file (default is stdout)") + cmd.Flags().BoolVar(&keyOpts.NoOut, "noout", false, "don't print key out") + cmd.Flags().BoolVar(&keyOpts.PubOut, "pubout", false, "print public key") + cmd.Flags().StringVar(&keyOpts.InForm, "inform", "pem", "input format - pem, short") + cmd.Flags().StringVar(&keyOpts.OutForm, "outform", "pem", "output format - pem, short") + return cmd +} + +func loadKeys(filename string, inform string) []interface{} { + var err error + var f *os.File + var keys []interface{} + + if strings.HasPrefix(filename, "ecc") { + if _, err := os.Stat(filename); os.IsNotExist(err) { + keys, err := ecc.UnmarshalShortKeys([]byte(filename)) + if err != nil { + log.Fatalf("parse short keys: %v", err) + } + return keys + } + } + + if filename == "" { + f = os.Stdin + } else { + f, err = os.Open(filename) + if err != nil { + log.Fatal(err) + } + defer f.Close() + } + switch inform { + case "pem": + keys, err = ecc.ReadPEM(f) + case "short": + var buf bytes.Buffer + _, err = io.Copy(&buf, f) + if err == nil { + keys, err = ecc.UnmarshalShortKeys(buf.Bytes()) + } + } + if err != nil { + log.Fatal("read key: ", err) + } + return keys +} + +func keyRun(opts *KeyOpts) { + var err error + keys := loadKeys(opts.In, opts.InForm) + if opts.NoOut || len(keys) == 0 { + return + } + var outKeys []interface{} + for _, key := range keys { + if priv, ok := key.(*ecdsa.PrivateKey); ok { + if opts.PubOut { + outKeys = append(outKeys, &priv.PublicKey) + } else { + outKeys = append(outKeys, priv) + } + } else if pub, ok := key.(*ecdsa.PublicKey); ok { + outKeys = append(outKeys, pub) + } + } + var perm os.FileMode = 0666 + for _, key := range keys { + if _, ok := key.(*ecdsa.PrivateKey); ok { + perm = 0600 + } + } + + var out *os.File + defer func() { + if err != nil { + log.Fatalf("could not create file: %v", err) + } + }() + if opts.Out == "" { + out = os.Stdout + } else { + if out, err = os.OpenFile(opts.Out, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm); err != nil { + return + } + defer func() { + if err == nil { + err = out.Close() + } + }() + } + for _, key := range outKeys { + switch keyOpts.OutForm { + case "pem": + err = ecc.WritePEM(key, out) + case "short": + var shortKey []byte + shortKey, err = ecc.MarshalShortKey(key) + if err != nil { + return + } + _, err = out.Write(shortKey) + if err != nil { + return + } + _, err = out.Write([]byte("\n")) + } + if err != nil { + return + } + } +} diff --git a/cmd/ecc/commands/root.go b/cmd/ecc/commands/root.go new file mode 100644 index 0000000..43bc2bb --- /dev/null +++ b/cmd/ecc/commands/root.go @@ -0,0 +1,21 @@ +package commands + +import ( + "github.com/spf13/cobra" +) + +func RootCmd(root *cobra.Command) *cobra.Command { + cmd := &cobra.Command{ + Use: "ecc", + Short: "Eliptic Curve Cryptography", + } + if root != nil { + root.AddCommand(cmd) + } + cmd.AddCommand(GenkeyCmd()) + cmd.AddCommand(KeyCmd()) + cmd.AddCommand(EncryptCmd()) + cmd.AddCommand(DecryptCmd()) + cmd.AddCommand(EditCmd()) + return cmd +} diff --git a/cmd/ecc/main.go b/cmd/ecc/main.go new file mode 100644 index 0000000..c1b5a07 --- /dev/null +++ b/cmd/ecc/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "os" + log "github.com/sirupsen/logrus" + "github.com/thepax/ecc/cmd/ecc/commands" +) + +type PlainFormatter struct { +} + +func (f *PlainFormatter) Format(entry *log.Entry) ([]byte, error) { + return []byte(entry.Message + "\n"), nil +} + +func main() { + log.SetFormatter(&PlainFormatter{}) + log.SetOutput(os.Stderr) + commands.RootCmd(nil).Execute() +} diff --git a/curve.go b/curve.go new file mode 100644 index 0000000..cb39560 --- /dev/null +++ b/curve.go @@ -0,0 +1,77 @@ +package ecc + +import ( + "crypto/elliptic" + "sync" +) + +type Curve struct { + Id uint8 + Name string + Aliases []string + Description string + Curve func() elliptic.Curve +} + +var Curves = []*Curve { + { + Id: 1, + Name: "P-224", + Aliases: []string{"secp224r1"}, + Description: "P-224 (FIPS 186-3, section D.2.2)", + Curve: elliptic.P224, + }, + { + Id: 2, + Name: "P-256", + Aliases: []string{"secp256r1", "prime256v1"}, + Description: "NIST P-256 (FIPS 186-3, section D.2.3)", + Curve: elliptic.P256, + }, + { + Id: 3, + Name: "P-384", + Aliases: []string{"secp384r1"}, + Description: "NIST P-384 (FIPS 186-3, section D.2.4)", + Curve: elliptic.P384, + }, + { + Id: 4, + Name: "P-521", + Aliases: []string{"secp521r1"}, + Description: "NIST P-521 (FIPS 186-3, section D.2.5)", + Curve: elliptic.P521, + }, +} + +var curvesIndexMutex sync.Mutex +var curvesIndex map[string]*Curve + +// LookupCurve finds Curve by specified name. +func LookupCurve(name string) *Curve { + curvesIndexMutex.Lock() + if curvesIndex == nil { + curvesIndex = make(map[string]*Curve) + for _, curve := range Curves { + curvesIndex[curve.Name] = curve + for _, alias := range curve.Aliases { + curvesIndex[alias] = curve + } + } + } + curvesIndexMutex.Unlock() + if curve, ok := curvesIndex[name]; ok { + return curve + } + return nil +} + +func IdentifyCurve(c elliptic.Curve) *Curve { + curveName := c.Params().Name + for _, curve := range Curves { + if curve.Name == curveName { + return curve + } + } + return nil +} diff --git a/decrypt.go b/decrypt.go new file mode 100644 index 0000000..db9d2f6 --- /dev/null +++ b/decrypt.go @@ -0,0 +1,197 @@ +package ecc + +import ( + "crypto/cipher" + "crypto/ecdsa" + "encoding/asn1" + "encoding/binary" + "io" + "sync" + "github.com/pkg/errors" +) + +type Decrypt struct { + PrivateKey *ecdsa.PrivateKey + VerifyKey *ecdsa.PublicKey + Input io.Reader + mutex sync.Mutex + version int + aead cipher.AEAD + buffer []byte + bufpos int + chunknum uint64 + err error +} + +func readASN1(r io.Reader) (packet []byte, err error) { + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } else { + err = errors.Wrapf(err, "read asn1") + } + }() + var buf [10]byte + hl := uint64(2) + if _, err = io.ReadFull(r, buf[:hl]); err != nil { + return + } + if (buf[0] & 0x1f) == 0x1f { + return nil, errors.New("tag too big") + } + var l uint64 + if (buf[1] & 0x80) == 0 { + l = uint64(buf[1]) + } else { + if buf[1] == 0xff || buf[1] == 0x80 { + return nil, errors.New("invalid length") + } + ll := uint64(buf[1] & 0x7f) + if ll > 8 { + return nil, errors.New("too long") + } + hl += ll + if _, err = io.ReadFull(r, buf[2:hl]); err != nil { + return + } + for i := 2; i < int(hl); i++ { + l = (l << 8) + uint64(buf[i]) + } + } + p := make([]byte, hl + l) + copy(p, buf[:hl]) + if _, err = io.ReadFull(r, p[hl:]); err != nil { + return + } + return p, nil +} + +func readHeader(r io.Reader) (version int, header *eccHeader, err error) { + defer func() { + err = errors.Wrapf(err, "read header") + }() + var hbuf [6]byte + if _, err = io.ReadFull(r, hbuf[:]); err != nil { + if err == io.ErrUnexpectedEOF { + err = ErrBadMagic + } + return + } + if version, err = MagicVersion(hbuf[:]); err != nil { + return + } + var buf []byte + if buf, err = readASN1(r); err != nil { + return + } + header, err = unmarshalHeader(buf) + return +} + +func (d *Decrypt) Read(p []byte) (n int, err error) { + defer func() { + if err != nil && d.err == nil { + d.err = err + } + if err != io.EOF { + err = errors.Wrapf(err, "ecc read") + } + }() + + d.mutex.Lock() + defer d.mutex.Unlock() + + if (d.err == nil || d.err == io.EOF) && d.bufpos < len(d.buffer) { + m := copy(p, d.buffer[d.bufpos:]) + n += m + d.bufpos += m + if d.err == io.EOF { + return n, nil + } + } + + if d.err != nil { + return 0, d.err + } + + if d.Input == nil { + return 0, io.EOF + } + + if d.aead == nil { + var header *eccHeader + d.version, header, err = readHeader(d.Input) + if err != nil { + return 0, err + } + if d.VerifyKey != nil { + if err = header.Verify(d.VerifyKey); err != nil { + return 0, err + } + } + pub, err := header.PublicKey() + if err != nil { + return 0, err + } + if pub.Curve.Params().Name != d.PrivateKey.Curve.Params().Name { + return 0, ErrInvalidKey + } + if !d.PrivateKey.Curve.IsOnCurve(pub.X, pub.Y) { + return 0, ErrInvalidKey + } + kdf, err := header.KDF() + if err != nil { + return 0, err + } + c, err := header.Cipher() + if err != nil { + return 0, err + } + key, err := DeriveKey(d.PrivateKey, pub, &d.PrivateKey.PublicKey, c.KeySize, kdf) + if err != nil { + return 0, err + } + d.aead, err = c.New(key) + if err != nil { + return 0, err + } + } + + for n < len(p) { + if d.bufpos >= len(d.buffer) { + if d.err == io.EOF { + return + } + var buf []byte + if buf, err = readASN1(d.Input); err != nil { + return + } + var chunk []byte + if _, err = asn1.Unmarshal(buf, &chunk); err != nil { + return n, ErrBadEncoding + } + nonce := make([]byte, d.aead.NonceSize()) + binary.LittleEndian.PutUint64(nonce, d.chunknum) + d.chunknum++ + d.bufpos = 0 + if d.buffer, err = d.aead.Open(nil, nonce, chunk, nil); err != nil { + nonce[len(nonce)-1] = 0x01 + if d.buffer, err = d.aead.Open(nil, nonce, chunk, nil); err != nil { + return + } + d.err = io.EOF + } + if len(d.buffer) == 0 { + d.err = io.EOF + if n > 0 { + return + } + return 0, io.EOF + } + } + m := copy(p[n:], d.buffer[d.bufpos:]) + n += m + d.bufpos += m + } + return +} diff --git a/ecc.go b/ecc.go new file mode 100644 index 0000000..992cf86 --- /dev/null +++ b/ecc.go @@ -0,0 +1,32 @@ +// Package ecc implements basic Elliptic Curve Cryptography functions. +// +package ecc + +import ( + "encoding/asn1" + "github.com/pkg/errors" +) + +// Default chunk size for encrypted streams. +const DefaultChunkSize = 65536 + +var ( + ErrBadEncoding = errors.New("bad encoding") + ErrBadMagic = errors.New("bad magic") + ErrInvalidCipher = errors.New("invalid cipher") + ErrInvalidKey = errors.New("invalid key") + ErrInvalidKeyType = errors.New("invalid key type") + ErrNotSigned = errors.New("not signed") + ErrUnableToLoadKey = errors.New("unable to load key") + ErrUnknownEncryption = errors.New("unknown encryption") + ErrUnknownKDF = errors.New("unknown key derivation function") + ErrUnsupportedCurve = errors.New("unsupported curve") + ErrVerifyFailed = errors.New("verify failed") +) + +var ( + oidEcdsaWithSha224 = asn1.ObjectIdentifier{1,2,840,10045,4,3,1} + oidEcdsaWithSha256 = asn1.ObjectIdentifier{1,2,840,10045,4,3,2} + oidEcdsaWithSha384 = asn1.ObjectIdentifier{1,2,840,10045,4,3,3} + oidEcdsaWithSha512 = asn1.ObjectIdentifier{1,2,840,10045,4,3,4} +) diff --git a/ecc_test.go b/ecc_test.go new file mode 100644 index 0000000..7944ebb --- /dev/null +++ b/ecc_test.go @@ -0,0 +1,118 @@ +package ecc + +import ( + "bytes" + "crypto/rand" + "io" + "strings" + "testing" +) + +func TestKeys(t *testing.T) { + curve := LookupCurve("P-256") + + priv1, err := GenerateKey(curve, rand.Reader) + if err != nil { + t.Fatal(err) + } + t.Logf("private key 1: %#v", priv1) + + priv2, err := GenerateKey(curve, rand.Reader) + if err != nil { + t.Fatal(err) + } + t.Logf("private key 2: %#v", priv2) + + kdf := KDFs["hkdf-sha256"] + + key1, err := DeriveKey(priv2, &priv1.PublicKey, &priv1.PublicKey, 32, kdf) + if err != nil { + t.Fatal(err) + } + t.Logf("key 1: %x", key1) + + key2, err := DeriveKey(priv1, &priv2.PublicKey, &priv1.PublicKey, 32, kdf) + if err != nil { + t.Fatal(err) + } + t.Logf("key 2: %x", key2) + + if bytes.Compare(key1, key2) != 0 { + t.Fatal("derived keys do not match") + } +} + +func TestEncryption(t *testing.T) { + curve := LookupCurve("P-256") + t.Logf("curve: %#v", curve) + + priv, err := GenerateKey(curve, rand.Reader) + if err != nil { + t.Fatal(err) + } + t.Logf("private key: %#v", priv) + + message := "Elliptic Curve Cryptography" + + var encrypted bytes.Buffer + enc := &Encrypt{ + PublicKey: &priv.PublicKey, + Cipher: "aes256-gcm", + Output: &encrypted, + } + _, err = io.Copy(enc, strings.NewReader(message)) + if err != nil { + t.Fatal(err) + } + err = enc.Close() + if err != nil { + t.Fatal(err) + } + t.Logf("encrypted: %x", encrypted.Bytes()) + + var decrypted bytes.Buffer + dec := &Decrypt{ + PrivateKey: priv, + Input: &encrypted, + } + t.Logf("decryptor: %#v", dec) + _, err = io.Copy(&decrypted, dec) + if err != nil { + t.Fatal(err) + } + + t.Logf("decrypted message: %s", decrypted.String()) + + if decrypted.String() != message { + t.Fatal("encryption error") + } +} + +func benchmarkEncrypt(b *testing.B, cipher string) { + priv, err := GenerateKey(LookupCurve("P-256"), rand.Reader) + if err != nil { + b.Fatal(err) + } + enc := &Encrypt{ + PublicKey: &priv.PublicKey, + Cipher: cipher, + } + var buf [1024*1024]byte + for i := 0; i < b.N; i++ { + _, err = enc.Write(buf[:]) + if err != nil { + b.Fatal(err) + } + } + err = enc.Close() + if err != nil { + b.Fatal(err) + } +} + +func BenchmarkEncrypt(b *testing.B) { + b.Run("aes-128-gcm", func(b *testing.B) { benchmarkEncrypt(b, "aes-128-gcm") }) + b.Run("aes-192-gcm", func(b *testing.B) { benchmarkEncrypt(b, "aes-192-gcm") }) + b.Run("aes-256-gcm", func(b *testing.B) { benchmarkEncrypt(b, "aes-256-gcm") }) + b.Run("chacha20-poly1305", func(b *testing.B) { benchmarkEncrypt(b, "chacha20-poly1305") }) +} diff --git a/eccutil/env.go b/eccutil/env.go new file mode 100644 index 0000000..32d354c --- /dev/null +++ b/eccutil/env.go @@ -0,0 +1,56 @@ +package eccutil + +import ( + "os" +) + +func GetenvKey() string { + return os.Getenv("ECC_KEY") +} + +func GetenvPrivateKey() string { + key := os.Getenv("ECC_PRIVATE_KEY") + if key == "" { + key = GetenvKey() + } + return key +} + +func GetenvPublicKey() string { + key := os.Getenv("ECC_PUBLIC_KEY") + if key == "" { + key = GetenvKey() + } + return key +} + +func GetenvEncryptKey() string { + key := os.Getenv("ECC_ENCRYPT_KEY") + if key == "" { + key = GetenvPublicKey() + } + return key +} +func GetenvSignKey() string { + key := os.Getenv("ECC_SIGN_KEY") + if key == "" { + key = GetenvPrivateKey() + } + return key +} + +func GetenvDecryptKey() string { + key := os.Getenv("ECC_DECRYPT_KEY") + if key == "" { + key = GetenvPrivateKey() + } + return key +} +func GetenvVerifyKey() string { + key := os.Getenv("ECC_VERIFY_KEY") + if key == "" { + key = GetenvPublicKey() + } + return key +} + diff --git a/eccutil/key.go b/eccutil/key.go new file mode 100644 index 0000000..324b8f2 --- /dev/null +++ b/eccutil/key.go @@ -0,0 +1,175 @@ +package eccutil + +import ( + "crypto/ecdsa" + "fmt" + "strconv" + "strings" + "github.com/pkg/errors" + "github.com/thepax/ecc" +) + +func ParseSCryptOpts(opts string) (*ecc.SCryptOpts, error) { + var err error + + so := ecc.NewDefaultSCryptOpts() + + o := strings.Split(opts, ":") + if len(o) > 0 && o[0] != "" { + so.Curve = ecc.LookupCurve(o[0]) + if so.Curve == nil { + return nil, ecc.ErrUnsupportedCurve + } + } + if len(o) > 1 && o[1] != "" { + so.N, err = strconv.Atoi(o[1]) + if err != nil { + return nil, errors.Wrapf(err, "N") + } + } + if len(o) > 2 && o[2] != "" { + so.R, err = strconv.Atoi(o[2]) + if err != nil { + return nil, errors.Wrapf(err, "r") + } + } + if len(o) > 3 && o[3] != "" { + so.P, err = strconv.Atoi(o[3]) + if err != nil { + return nil, errors.Wrapf(err, "p") + } + } + if len(o) > 4 && o[4] != "" { + so.Salt = []byte(o[4]) + } + return so, nil +} + +var passwordKeys map[string]*ecdsa.PrivateKey +var sshAgentKeys map[string]*ecdsa.PrivateKey + +func GetPrivateKey(purpose, key string) (*ecdsa.PrivateKey, error) { + var privateKey *ecdsa.PrivateKey + var err error + + if key == "" { + return nil, errors.Errorf("no %s defined", purpose) + } + + if strings.HasPrefix(key, "ecc") { + privateKey, err = ecc.UnmarshalShortPrivateKey([]byte(key)) + if err != nil { + return nil, errors.Wrapf(err, "parse %s key", purpose) + } + return privateKey, nil + } + if strings.HasPrefix(key, "password:") { + if passwordKeys == nil { + passwordKeys = make(map[string]*ecdsa.PrivateKey) + } + if passwordKeys[key] != nil { + return passwordKeys[key], nil + } + scryptOpts, err := ParseSCryptOpts(key[9:]) + if err != nil { + return nil, errors.Wrap(err, "parse scrypt options") + } + var password []byte + if purpose == "decrypting" { + password, err = GetPassword("Password: ") + } else { + password, err = NewPassword(fmt.Sprintf("%s Password: ", strings.Title(purpose))) + } + if err != nil { + return nil, errors.Wrapf(err, "%s password", purpose) + } + privateKey, err = ecc.GenerateKeyFromPassword(password, scryptOpts) + if err != nil { + return nil, errors.Wrapf(err, "derive %s key", purpose) + } + passwordKeys[key] = privateKey + return privateKey, nil + } + if strings.HasPrefix(key, "ssh-agent:") { + if sshAgentKeys == nil { + sshAgentKeys = make(map[string]*ecdsa.PrivateKey) + } + if sshAgentKeys[key] != nil { + return sshAgentKeys[key], nil + } + privateKey, err = SSHAgentKey(key[10:]) + if err != nil { + return nil, err + } + sshAgentKeys[key] = privateKey + return privateKey, nil + } + privateKey, err = ecc.LoadPrivateKeyPEM(key) + if err != nil { + return nil, errors.Wrapf(err, "load %s key", purpose) + } + return privateKey, nil +} + +func GetPublicKey(purpose, key string) (*ecdsa.PublicKey, error) { + var publicKey *ecdsa.PublicKey + var err error + + if key == "" { + return nil, errors.Errorf("no %s defined", purpose) + } + + if strings.HasPrefix(key, "ecc") { + publicKey, err = ecc.UnmarshalShortPublicKey([]byte(key)) + if err != nil { + return nil, errors.Wrapf(err, "parse %s key", purpose) + } + return publicKey, nil + } + if strings.HasPrefix(key, "password:") { + if passwordKeys == nil { + passwordKeys = make(map[string]*ecdsa.PrivateKey) + } + if passwordKeys[key] != nil { + return &passwordKeys[key].PublicKey, nil + } + scryptOpts, err := ParseSCryptOpts(key[9:]) + if err != nil { + return nil, errors.Wrap(err, "parse scrypt options") + } + var password []byte + if purpose == "encrypting" { + password, err = NewPassword("Password: ") + } else { + password, err = GetPassword(fmt.Sprintf("%s Password: ", strings.Title(purpose))) + } + if err != nil { + return nil, errors.Wrapf(err, "%s password", purpose) + } + privateKey, err := ecc.GenerateKeyFromPassword(password, scryptOpts) + if err != nil { + return nil, errors.Wrapf(err, "derive %s key", purpose) + } + passwordKeys[key] = privateKey + return &privateKey.PublicKey, nil + } + if strings.HasPrefix(key, "ssh-agent:") { + if sshAgentKeys == nil { + sshAgentKeys = make(map[string]*ecdsa.PrivateKey) + } + if sshAgentKeys[key] != nil { + return &sshAgentKeys[key].PublicKey, nil + } + privateKey, err := SSHAgentKey(key[10:]) + if err != nil { + return nil, err + } + sshAgentKeys[key] = privateKey + return &privateKey.PublicKey, nil + } + publicKey, err = ecc.LoadPublicKeyPEM(key) + if err != nil { + return nil, errors.Wrapf(err, "load %s key", purpose) + } + return publicKey, nil +} diff --git a/eccutil/password.go b/eccutil/password.go new file mode 100644 index 0000000..18cbc76 --- /dev/null +++ b/eccutil/password.go @@ -0,0 +1,51 @@ +package eccutil + +import ( + "os" + "github.com/pkg/errors" + "golang.org/x/term" +) + +var ( + ErrPasswordsDontMatch = errors.New("passwords don't match") + ErrTerminalNotAvailable = errors.New("terminal is not available") +) + +func GetPassword(prompt string) ([]byte, error) { + if !term.IsTerminal(int(os.Stdin.Fd())) { + return nil, ErrTerminalNotAvailable + } + out := os.Stdout + if !term.IsTerminal(int(out.Fd())) { + out = os.Stderr + if !term.IsTerminal(int(out.Fd())) { + return nil, ErrTerminalNotAvailable + } + } + if _, err := out.Write([]byte(prompt)); err != nil { + return nil, err + } + password, err := term.ReadPassword(int(os.Stdin.Fd())) + out.Write([]byte("\n")) + return password, err +} + +func NewPassword(prompt string) ([]byte, error) { + password1, err := GetPassword(prompt) + if err != nil { + return nil, err + } + password2, err := GetPassword("Again - " + prompt) + if err != nil { + return nil, err + } + if len(password1) != len(password2) { + return nil, ErrPasswordsDontMatch + } + for i := 0; i < len(password1); i++ { + if password1[i] != password2[i] { + return nil, ErrPasswordsDontMatch + } + } + return password1, nil +} diff --git a/eccutil/ssh-agent.go b/eccutil/ssh-agent.go new file mode 100644 index 0000000..2751d00 --- /dev/null +++ b/eccutil/ssh-agent.go @@ -0,0 +1,61 @@ +package eccutil + +import ( + "crypto/ecdsa" + "crypto/sha256" + "encoding/base64" + "net" + "os" + "strings" + "github.com/pkg/errors" + "golang.org/x/crypto/ssh/agent" + "github.com/thepax/ecc" +) + +func SSHAgentKey(opts string) (*ecdsa.PrivateKey, error) { + o := strings.Split(opts, ":") + if len(o) != 3 { + return nil, errors.Errorf("invalid ssh agent options: %s", opts) + } + if o[0] == "" { + o[0] = "secp521r1" + } + curve := ecc.LookupCurve(o[0]) + if curve == nil { + return nil,errors.Wrapf(ecc.ErrUnsupportedCurve, "ssh agent options") + } + pubKey := o[1] + var salt []byte + if o[2] != "" { + salt = []byte(o[2]) + } else { + salt = []byte(o[1]) + } + + sshAuthSock := os.Getenv("SSH_AUTH_SOCK") + if sshAuthSock == "" { + return nil, errors.New("no ssh agent found") + } + conn, err := net.Dial("unix", sshAuthSock) + if err != nil { + return nil, errors.Wrapf(err, "connect to ssh agent") + } + defer conn.Close() + sshAgent := agent.NewClient(conn) + signers, err := sshAgent.Signers() + if err != nil { + return nil, errors.Wrapf(err, "get ssh agent keys") + } + for _, signer := range signers { + hash := sha256.New() + hash.Write(signer.PublicKey().Marshal()) + if base64.RawStdEncoding.EncodeToString(hash.Sum(nil)) == pubKey { + signature, err := signer.Sign(nil, salt) + if err != nil { + return nil, errors.Wrap(err, "ssh agent sign") + } + return ecc.GenerateKeyFromData(curve, signature.Blob, salt) + } + } + return nil, errors.New("ssh agent key not found") +} diff --git a/encrypt.go b/encrypt.go new file mode 100644 index 0000000..e4888e2 --- /dev/null +++ b/encrypt.go @@ -0,0 +1,155 @@ +package ecc + +import ( + "crypto/cipher" + "crypto/ecdsa" + "crypto/rand" + "encoding/asn1" + "encoding/binary" + "io" + "io/ioutil" + "sync" + "github.com/pkg/errors" +) + +type Encrypt struct { + PublicKey *ecdsa.PublicKey + SignKey *ecdsa.PrivateKey + Cipher interface{} + Output io.Writer + ChunkSize uint32 + Random io.Reader + mutex sync.Mutex + buffer []byte + bufpos int + aead cipher.AEAD + chunknum uint64 + err error +} + +func (e *Encrypt) Write(p []byte) (int, error) { + e.mutex.Lock() + defer e.mutex.Unlock() + + if e.err != nil { + return 0, e.err + } + + if e.buffer == nil { + bufferSize := e.ChunkSize + if bufferSize == 0 { + bufferSize = DefaultChunkSize + } + e.buffer = make([]byte, bufferSize) + } + + var n int + for n < len(p) { + if e.bufpos == len(e.buffer) { + err := e.flush(false) + if err != nil { + return 0, err + } + } + m := copy(e.buffer[e.bufpos:], p[n:]) + e.bufpos += m + n += m + } + + return n, nil +} + +func (e *Encrypt) flush(last bool) error { + if e.err != nil { + return e.err + } + + w := e.Output + // avoid failing if Output is not defined + if w == nil { + w = ioutil.Discard + } + + if e.aead == nil { + if e.PublicKey == nil { + return ErrInvalidKey + } + var c *Cipher + switch e.Cipher.(type) { + case string: + c = LookupCipher(e.Cipher.(string)) + case *Cipher: + c = e.Cipher.(*Cipher) + } + if c == nil { + return ErrInvalidCipher + } + kdf := c.KDF + rr := e.Random + if rr == nil { + rr = rand.Reader + } + priv, err := ecdsa.GenerateKey(e.PublicKey.Curve, rr) + if err != nil { + return errors.Wrapf(err, "generate private key") + } + key, err := DeriveKey(priv, e.PublicKey, e.PublicKey, c.KeySize, kdf) + if err != nil { + return err + } + e.aead, err = c.New(key) + if err != nil { + return err + } + if _, err = w.Write(Magic); err != nil { + e.err = err + return err + } + header, err := newHeader(&priv.PublicKey, kdf.OID, c.OID) + if err != nil { + e.err = errors.Wrapf(err, "create header") + return e.err + } + if e.SignKey != nil { + if err = header.Sign(e.SignKey, rr); err != nil { + e.err = errors.Wrapf(err, "sign") + return e.err + } + } + asn1header, err := header.Marshal() + if err != nil { + e.err = errors.Wrapf(err, "marshal header") + return e.err + } + if _, err = w.Write(asn1header); err != nil { + e.err = err + return err + } + } + + nonce := make([]byte, e.aead.NonceSize()) + binary.LittleEndian.PutUint64(nonce, e.chunknum) + if last { + nonce[len(nonce)-1] = 0x01 + e.err = io.ErrClosedPipe + } + encrypted := e.aead.Seal(nil, nonce, e.buffer[:e.bufpos], nil) + chunk, err := asn1.Marshal(encrypted) + if err != nil { + e.err = errors.Wrapf(err, "encrypt: flush") + return e.err + } + if _, err := w.Write(chunk); err != nil { + e.err = err + return err + } + e.bufpos = 0 + e.chunknum++ + return nil +} + +func (e *Encrypt) Close() error { + e.mutex.Lock() + defer e.mutex.Unlock() + return e.flush(true) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..beec4bf --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/thepax/ecc + +go 1.19 + +require ( + github.com/pkg/errors v0.9.1 + github.com/sirupsen/logrus v1.9.0 + github.com/spf13/cobra v1.5.0 + golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 +) + +require ( + github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2c075d6 --- /dev/null +++ b/go.sum @@ -0,0 +1,33 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= +github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo= +golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM= +golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/header.go b/header.go new file mode 100644 index 0000000..b3c718d --- /dev/null +++ b/header.go @@ -0,0 +1,152 @@ +package ecc + +import ( + "crypto/ecdsa" + "crypto/sha256" + "crypto/sha512" + "crypto/x509" + "encoding/asn1" + "io" + "github.com/pkg/errors" +) + +// The ecc magic sequence. +var Magic = []byte{0x13, 0x04, 0x65, 0x63, 0x63, 0x31} + +// IsMagic returns true if buf starts with an ecc stream. +func IsMagic(buf []byte) bool { + return len(buf) >= 6 && + buf[0] == 0x13 && buf[1] == 0x04 && + buf[2] == 0x65 && buf[3] == 0x63 && buf[4] == 0x63 && + buf[5] == 0x31 +} + +func MagicVersion(buf []byte) (int, error) { + if !IsMagic(buf) { + return -1, ErrBadMagic + } + return int(buf[5] - 0x30), nil +} + +type eccHeader struct { + PKIXPublicKey []byte + Ciphers []asn1.ObjectIdentifier + Signature []byte +} + +func newHeader(pub *ecdsa.PublicKey, ciphers ...asn1.ObjectIdentifier) (*eccHeader, error) { + pkixPub, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return nil, err + } + return &eccHeader{ + PKIXPublicKey: pkixPub, + Ciphers: ciphers, + }, nil +} + +func unmarshalHeader(buf []byte) (*eccHeader, error) { + header := &eccHeader{} + if _, err := asn1.Unmarshal(buf, header); err != nil { + return nil, errors.New("invalid header") + } + return header, nil +} + +func (h *eccHeader) PublicKey() (*ecdsa.PublicKey, error) { + pub, err := x509.ParsePKIXPublicKey(h.PKIXPublicKey) + if err != nil { + return nil, err + } + if ecdsaPub, ok := pub.(*ecdsa.PublicKey); ok { + return ecdsaPub, nil + } + return nil, ErrInvalidKeyType +} + +func (h *eccHeader) Cipher() (*Cipher, error) { + for _, oid := range h.Ciphers { + for _, c := range Ciphers { + if c.OID.Equal(oid) { + return c, nil + } + } + } + return nil, ErrUnknownEncryption +} + +func (h *eccHeader) KDF() (*KDF, error) { + for _, oid := range h.Ciphers { + for _, kdf := range KDFs { + if kdf.OID.Equal(oid) { + return kdf, nil + } + } + } + return nil, ErrUnknownKDF +} + +func (h *eccHeader) Marshal() ([]byte, error) { + return asn1.Marshal(*h) +} + +func (h *eccHeader) Sign(priv *ecdsa.PrivateKey, rr io.Reader) (err error) { + var hash []byte + switch { + case priv.Curve.Params().BitSize <= 224: + h.Ciphers = append(h.Ciphers, oidEcdsaWithSha224) + sum := sha256.Sum224(h.PKIXPublicKey) + hash = sum[:] + case priv.Curve.Params().BitSize <= 256: + h.Ciphers = append(h.Ciphers, oidEcdsaWithSha256) + sum := sha256.Sum256(h.PKIXPublicKey) + hash = sum[:] + case priv.Curve.Params().BitSize <= 384: + h.Ciphers = append(h.Ciphers, oidEcdsaWithSha384) + sum := sha512.Sum384(h.PKIXPublicKey) + hash = sum[:] + default: + h.Ciphers = append(h.Ciphers, oidEcdsaWithSha512) + sum := sha512.Sum512(h.PKIXPublicKey) + hash = sum[:] + } + h.Signature, err = ecdsa.SignASN1(rr, priv, hash) + return err +} + +func (h *eccHeader) Verify(pub *ecdsa.PublicKey) error { + if h.Signature == nil { + return ErrNotSigned + } + for _, cipher := range h.Ciphers { + if cipher.Equal(oidEcdsaWithSha224) { + sum := sha256.Sum224(h.PKIXPublicKey) + if ecdsa.VerifyASN1(pub, sum[:], h.Signature) { + return nil + } + return ErrVerifyFailed + } + if cipher.Equal(oidEcdsaWithSha256) { + sum := sha256.Sum256(h.PKIXPublicKey) + if ecdsa.VerifyASN1(pub, sum[:], h.Signature) { + return nil + } + return ErrVerifyFailed + } + if cipher.Equal(oidEcdsaWithSha384) { + sum := sha512.Sum384(h.PKIXPublicKey) + if ecdsa.VerifyASN1(pub, sum[:], h.Signature) { + return nil + } + return ErrVerifyFailed + } + if cipher.Equal(oidEcdsaWithSha512) { + sum := sha512.Sum512(h.PKIXPublicKey) + if ecdsa.VerifyASN1(pub, sum[:], h.Signature) { + return nil + } + return ErrVerifyFailed + } + } + return ErrNotSigned +} diff --git a/kdf.go b/kdf.go new file mode 100644 index 0000000..b0c368c --- /dev/null +++ b/kdf.go @@ -0,0 +1,74 @@ +package ecc + +import ( + "crypto/ecdsa" + "encoding/asn1" + "crypto/sha256" + "crypto/sha512" + "io" + "math/big" + "github.com/pkg/errors" + "golang.org/x/crypto/hkdf" +) + +type KDF struct { + Name string + Description string + OID asn1.ObjectIdentifier + HashSize int + New func(secret []byte) io.Reader +} + +// Supported Key Derivation Functions +// (see RFC8619) +var KDFs = map[string]*KDF { + "hkdf-sha256": &KDF{ + Name: "hkdf-sha256", + Description: "HMAC-based Key Derivation Function with SHA256", + OID: asn1.ObjectIdentifier{1,2,840,113549,1,9,16,3,28}, + HashSize: 32, + New: func(secret []byte) io.Reader { + return hkdf.New(sha256.New, secret, nil, nil) + }, + }, + "hkdf-sha384": &KDF{ + Name: "hkdf-sha384", + Description: "HMAC-based Key Derivation Function with SHA384", + OID: asn1.ObjectIdentifier{1,2,840,113549,1,9,16,3,29}, + HashSize: 48, + New: func(secret []byte) io.Reader { + return hkdf.New(sha512.New384, secret, nil, nil) + }, + }, + "hkdf-sha512": &KDF{ + Name: "hkdf-sha512", + Description: "HMAC-based Key Derivation Function with SHA512", + OID: asn1.ObjectIdentifier{1,2,840,113549,1,9,16,3,30}, + HashSize: 64, + New: func(secret []byte) io.Reader { + return hkdf.New(sha512.New, secret, nil, nil) + }, + }, +} + +// Derives keys for symmetric encryption/decryption using ECDH. +func DeriveKey(priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, pub2 *ecdsa.PublicKey, size int, kdf *KDF) ([]byte, error) { + sx, sy := pub.Curve.ScalarMult(pub.X, pub.Y, priv.D.Bytes()) + var secret = struct { + PX *big.Int + PY *big.Int + SX *big.Int + SY *big.Int + }{ pub2.X, pub2.Y, sx, sy } + s, err := asn1.Marshal(secret) + if err != nil { + return nil, errors.Wrapf(err, "marshal secret") + } + kdfr := kdf.New(s) + key := make([]byte, size) + _, err = io.ReadFull(kdfr, key) + if err != nil { + return nil, errors.New("cannot derive key") + } + return key, nil +} diff --git a/key.go b/key.go new file mode 100644 index 0000000..d2c54d3 --- /dev/null +++ b/key.go @@ -0,0 +1,71 @@ +package ecc + +import ( + "bytes" + "crypto/ecdsa" + "crypto/sha512" + "io" + "golang.org/x/crypto/hkdf" + "golang.org/x/crypto/scrypt" +) + +// GenerateKey generates ECDSA key from specified Curve. +func GenerateKey(c *Curve, rr io.Reader) (*ecdsa.PrivateKey, error) { + return ecdsa.GenerateKey(c.Curve(), rr) +} + +func GenerateCoolKey(c *Curve, rr io.Reader) (*ecdsa.PrivateKey, error) { + for { + priv, err := ecdsa.GenerateKey(c.Curve(), rr) + if err != nil { + return nil, err + } + short, err := MarshalShortPrivateKey(priv) + if err != nil { + return nil, err + } + if bytes.IndexAny(short, "_-+/") >= 0 { + continue + } + short, err = MarshalShortPublicKey(&priv.PublicKey) + if err != nil { + return nil, err + } + if bytes.IndexAny(short, "_-+/") < 0 { + return priv, nil + } + } +} + +func GenerateKeyFromData(c *Curve, data []byte, salt []byte) (*ecdsa.PrivateKey, error) { + return GenerateKey(c, hkdf.New(sha512.New, data, salt, nil)) +} + +type SCryptOpts struct { + Curve *Curve + N int + R int + P int + Salt []byte +} + +func NewDefaultSCryptOpts() *SCryptOpts { + return &SCryptOpts{ + Curve: LookupCurve("secp521r1"), + N: 1<<20, + R: 8, + P: 1, + Salt: nil, + } +} + +func GenerateKeyFromPassword(password []byte, scryptOpts *SCryptOpts) (*ecdsa.PrivateKey, error) { + if scryptOpts == nil { + scryptOpts = NewDefaultSCryptOpts() + } + key, err := scrypt.Key(password, scryptOpts.Salt, scryptOpts.N, scryptOpts.R, scryptOpts.P, 1048576) + if err != nil { + return nil, err + } + return GenerateKeyFromData(scryptOpts.Curve, key, scryptOpts.Salt) +} diff --git a/pem.go b/pem.go new file mode 100644 index 0000000..84ddb65 --- /dev/null +++ b/pem.go @@ -0,0 +1,155 @@ +package ecc + +import ( + "bytes" + "crypto/ecdsa" + "crypto/x509" + "encoding/pem" + "io" + "os" + "github.com/pkg/errors" +) + +func WritePEM(key interface{}, w io.Writer) error { + switch key.(type) { + case *ecdsa.PrivateKey: + der, err := x509.MarshalECPrivateKey(key.(*ecdsa.PrivateKey)) + if err != nil { + return errors.Wrapf(err, "write private key PEM: marshal EC private key") + } + return errors.Wrapf(pem.Encode(w, &pem.Block{Type: "EC PRIVATE KEY", Bytes: der}), "write private key PEM: encode") + case *ecdsa.PublicKey: + der, err := x509.MarshalPKIXPublicKey(key.(*ecdsa.PublicKey)) + if err != nil { + return errors.Wrapf(err, "write PEM: marshal EC public key") + } + return errors.Wrapf(pem.Encode(w, &pem.Block{Type: "PUBLIC KEY", Bytes: der}), "write PEM: encode") + } + return ErrInvalidKeyType +} + +// ReadPEM reads either ecdsa.PrivateKey or ecdsa.PublicKey from io.Reader. +// ReadPEM returns ErrUnableToLoadKey if io.Reader doesn't contain ecdsa.PrivateKey or ecdsa.PublicKey. +func ReadPEM(r io.Reader) ([]interface{}, error) { + var result []interface{} + var buf bytes.Buffer + if _, err := io.Copy(&buf, r); err != nil { + return nil, errors.Wrapf(err, "read pem") + } + rest := buf.Bytes() + for { + var block *pem.Block + block, rest = pem.Decode(rest) + if block == nil { + if result == nil { + return nil, ErrUnableToLoadKey + } + return result, nil + } + switch block.Type { + case "EC PRIVATE KEY": + priv, err := x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return nil, errors.Wrapf(err, "parse EC private key") + } + result = append(result, priv) + case "PUBLIC KEY": + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, errors.Wrapf(err, "parse public key") + } + if ecdsaPub, ok := pub.(*ecdsa.PublicKey); ok { + result = append(result, ecdsaPub) + } + } + } +} + +// ReadPrivateKeyPEM reads ecdsa.PrivateKey from io.Reader. +// ReadPrivateKeyPEM returns ErrUnableToLoadKey if io.Reader doesn't contain ecdsa.PrivateKey. +func ReadPrivateKeyPEM(r io.Reader) (*ecdsa.PrivateKey, error) { + keys, err := ReadPEM(r) + if err != nil { + return nil, err + } + for _, key := range keys { + if ecdsaPriv, ok := key.(*ecdsa.PrivateKey); ok { + return ecdsaPriv, nil + } + } + return nil, ErrUnableToLoadKey +} + +// ReadPublicKeyPEM reads ecdsa.PublicKey from io.Reader. +// ReadPublicKeyPEM returns ErrUnableToLoadKey if io.Reader doesn't contain ecdsa.PublicKey. +func ReadPublicKeyPEM(r io.Reader) (*ecdsa.PublicKey, error) { + keys, err := ReadPEM(r) + if err != nil { + return nil, err + } + var priv *ecdsa.PrivateKey + for _, key := range keys { + if ecdsaPub, ok := key.(*ecdsa.PublicKey); ok { + return ecdsaPub, nil + } + if priv == nil { + if ecdsaPriv, ok := key.(*ecdsa.PrivateKey); ok { + priv = ecdsaPriv + } + } + } + if priv != nil { + return &priv.PublicKey, nil + } + return nil, ErrUnableToLoadKey +} + +func ReadKeyPair(r io.Reader) (*ecdsa.PrivateKey, *ecdsa.PublicKey, error) { + var priv *ecdsa.PrivateKey + var pub *ecdsa.PublicKey + keys, err := ReadPEM(r) + if err != nil { + return nil, nil, err + } + for _, key := range keys { + if priv == nil { + if ecdsaPriv, ok := key.(*ecdsa.PrivateKey); ok { + priv = ecdsaPriv + } + } + if pub == nil { + if ecdsaPub, ok := key.(*ecdsa.PublicKey); ok { + pub = ecdsaPub + } + } + } + if priv == nil { + return nil, nil, ErrUnableToLoadKey + } + if pub == nil { + pub = &priv.PublicKey + } + return priv, pub, nil +} + +// LoadPrivateKeyPEM reads ecdsa.PrivateKey from a file. +// LoadPrivateKeyPEM returns ErrUnableToLoadKey if the file doesn't contain ecdsa.PrivateKey. +func LoadPrivateKeyPEM(filename string) (*ecdsa.PrivateKey, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + return ReadPrivateKeyPEM(f) +} + +// LoadPublicKeyPEM reads ecdsa.PublicKey from a file. +// LoadPublicKeyPEM returns ErrUnableToLoadKey if the file doesn't contain ecdsa.PublicKey. +func LoadPublicKeyPEM(filename string) (*ecdsa.PublicKey, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + return ReadPublicKeyPEM(f) +} diff --git a/short.go b/short.go new file mode 100644 index 0000000..71251a1 --- /dev/null +++ b/short.go @@ -0,0 +1,172 @@ +package ecc + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "encoding/base64" + "math/big" + "regexp" +) + +var crc8table = [256]byte{ + 0x00, 0x07, 0x0e, 0x09, 0x1c, 0x1b, 0x12, 0x15, + 0x38, 0x3f, 0x36, 0x31, 0x24, 0x23, 0x2a, 0x2d, + 0x70, 0x77, 0x7e, 0x79, 0x6c, 0x6b, 0x62, 0x65, + 0x48, 0x4f, 0x46, 0x41, 0x54, 0x53, 0x5a, 0x5d, + 0xe0, 0xe7, 0xee, 0xe9, 0xfc, 0xfb, 0xf2, 0xf5, + 0xd8, 0xdf, 0xd6, 0xd1, 0xc4, 0xc3, 0xca, 0xcd, + 0x90, 0x97, 0x9e, 0x99, 0x8c, 0x8b, 0x82, 0x85, + 0xa8, 0xaf, 0xa6, 0xa1, 0xb4, 0xb3, 0xba, 0xbd, + 0xc7, 0xc0, 0xc9, 0xce, 0xdb, 0xdc, 0xd5, 0xd2, + 0xff, 0xf8, 0xf1, 0xf6, 0xe3, 0xe4, 0xed, 0xea, + 0xb7, 0xb0, 0xb9, 0xbe, 0xab, 0xac, 0xa5, 0xa2, + 0x8f, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9d, 0x9a, + 0x27, 0x20, 0x29, 0x2e, 0x3b, 0x3c, 0x35, 0x32, + 0x1f, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0d, 0x0a, + 0x57, 0x50, 0x59, 0x5e, 0x4b, 0x4c, 0x45, 0x42, + 0x6f, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7d, 0x7a, + 0x89, 0x8e, 0x87, 0x80, 0x95, 0x92, 0x9b, 0x9c, + 0xb1, 0xb6, 0xbf, 0xb8, 0xad, 0xaa, 0xa3, 0xa4, + 0xf9, 0xfe, 0xf7, 0xf0, 0xe5, 0xe2, 0xeb, 0xec, + 0xc1, 0xc6, 0xcf, 0xc8, 0xdd, 0xda, 0xd3, 0xd4, + 0x69, 0x6e, 0x67, 0x60, 0x75, 0x72, 0x7b, 0x7c, + 0x51, 0x56, 0x5f, 0x58, 0x4d, 0x4a, 0x43, 0x44, + 0x19, 0x1e, 0x17, 0x10, 0x05, 0x02, 0x0b, 0x0c, + 0x21, 0x26, 0x2f, 0x28, 0x3d, 0x3a, 0x33, 0x34, + 0x4e, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5c, 0x5b, + 0x76, 0x71, 0x78, 0x7f, 0x6a, 0x6d, 0x64, 0x63, + 0x3e, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2c, 0x2b, + 0x06, 0x01, 0x08, 0x0f, 0x1a, 0x1d, 0x14, 0x13, + 0xae, 0xa9, 0xa0, 0xa7, 0xb2, 0xb5, 0xbc, 0xbb, + 0x96, 0x91, 0x98, 0x9f, 0x8a, 0x8d, 0x84, 0x83, + 0xde, 0xd9, 0xd0, 0xd7, 0xc2, 0xc5, 0xcc, 0xcb, + 0xe6, 0xe1, 0xe8, 0xef, 0xfa, 0xfd, 0xf4, 0xf3, +} + +func crc8(data []byte) byte { + var crc byte + for _, d := range data { + crc = crc8table[d ^ crc] + } + return crc +} + +func MarshalShortPrivateKey(key *ecdsa.PrivateKey) ([]byte, error) { + var buf bytes.Buffer + curve := IdentifyCurve(key.Curve) + if curve == nil { + return nil, ErrUnsupportedCurve + } + b64 := base64.NewEncoder(base64.RawURLEncoding, &buf) + b64.Write([]byte{0x79, 0xc7, curve.Id | 0x20, crc8(key.D.Bytes())}) + b64.Write(key.D.Bytes()) + b64.Close() + return buf.Bytes(), nil +} + +func MarshalShortPublicKey(key *ecdsa.PublicKey) ([]byte, error) { + var buf bytes.Buffer + curve := IdentifyCurve(key.Curve) + if curve == nil { + return nil, ErrUnsupportedCurve + } + b64 := base64.NewEncoder(base64.RawURLEncoding, &buf) + data := elliptic.MarshalCompressed(key.Curve, key.X, key.Y) + b64.Write([]byte{0x79, 0xc7, curve.Id, crc8(data)}) + b64.Write(data) + b64.Close() + return buf.Bytes(), nil +} + +func MarshalShortKey(key interface{}) ([]byte, error) { + if priv, ok := key.(*ecdsa.PrivateKey); ok { + return MarshalShortPrivateKey(priv) + } + if pub, ok := key.(*ecdsa.PublicKey); ok { + return MarshalShortPublicKey(pub) + } + return nil, ErrInvalidKey +} + +func UnmarshalShortKeys(s []byte) ([]interface{}, error) { + var keys []interface{} + for _, match := range regexp.MustCompile(`\becc[0-9A-Za-z_-]+`).FindAllStringSubmatch(string(s), -1) { + k := []byte(match[0]) + if len(k) < 8 { + return nil, ErrInvalidKey + } + buf := make([]byte, base64.RawURLEncoding.DecodedLen(len(k))) + _, err := base64.RawURLEncoding.Decode(buf, k) + if err != nil { + return nil, err + } + if len(buf) < 5 || buf[0] != 0x79 || buf[1] != 0xc7 { + return nil, ErrInvalidKey + } + var curve elliptic.Curve + for _, c := range Curves { + if c.Id == (buf[2] & 0x1f) { + curve = c.Curve() + } + } + if curve == nil { + return nil, ErrUnsupportedCurve + } + if buf[3] != crc8(buf[4:]) { + return nil, ErrInvalidKey + } + if buf[2] & 0x20 != 0 { + priv := new(ecdsa.PrivateKey) + priv.D = big.NewInt(0) + priv.D.SetBytes(buf[4:]) + priv.PublicKey.Curve = curve + priv.PublicKey.X, priv.PublicKey.Y = curve.ScalarBaseMult(buf[4:]) + keys = append(keys, priv) + } else { + pub := new(ecdsa.PublicKey) + pub.Curve = curve + if buf[4] == 2 || buf[4] == 3 { + pub.X, pub.Y = elliptic.UnmarshalCompressed(curve, buf[4:]) + } else if buf[4] == 4 { + pub.X, pub.Y = elliptic.Unmarshal(curve, buf[4:]) + } + if pub.X == nil || pub.Y == nil { + return nil, ErrInvalidKey + } + keys = append(keys, pub) + } + } + return keys, nil +} + +func UnmarshalShortPrivateKey(s []byte) (*ecdsa.PrivateKey, error) { + keys, err := UnmarshalShortKeys(s) + if err != nil { + return nil, err + } + for _, key := range keys { + if priv, ok := key.(*ecdsa.PrivateKey); ok { + return priv, nil + } + } + return nil, ErrInvalidKey +} + +func UnmarshalShortPublicKey(s []byte) (*ecdsa.PublicKey, error) { + keys, err := UnmarshalShortKeys(s) + if err != nil { + return nil, err + } + for _, key := range keys { + if pub, ok := key.(*ecdsa.PublicKey); ok { + return pub, nil + } + } + for _, key := range keys { + if priv, ok := key.(*ecdsa.PrivateKey); ok { + return &priv.PublicKey, nil + } + } + return nil, ErrInvalidKey +}