diff --git a/lesson_7/cmd/keys/generate.go b/lesson_7/cmd/keys/generate.go new file mode 100644 index 0000000..ba8e3df --- /dev/null +++ b/lesson_7/cmd/keys/generate.go @@ -0,0 +1,167 @@ +package keys + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "os" + "path/filepath" + + "lesson7/crypto_utils" + "lesson7/logger" + "lesson7/utils" + + "github.com/spf13/cobra" +) + +type PrivateKeyGen struct { + outputPath string + keyBitSize int + saltSize int +} + +func init() { + keysCmd.AddCommand(keysGenerateCmd) + + keysGenerateCmd.Flags().String("pub-out", "pub_key.pem", "Path to save the public key") + keysGenerateCmd.Flags().String("priv-out", "priv_key.pem", "Path to save the private key") + keysGenerateCmd.Flags().Int("priv-size", 2048, "Private key size in bits") + keysGenerateCmd.Flags().Int("salt-size", 16, "Salt size used in key derivation in bytes") +} + +var keysGenerateCmd = &cobra.Command{ + Use: "generate", + Short: "Generates key pair.", + Long: `Generate an RSA key pair and store it in PEM files. The private key will be encrypted using a passphrase that you'll need to enter. AES encryption with Argon2 key derivation function is utilized.`, + Run: func(cmd *cobra.Command, args []string) { + pkOut, _ := cmd.Flags().GetString("priv-out") + pkSize, _ := cmd.Flags().GetInt("priv-size") + saltSize, _ := cmd.Flags().GetInt("salt-size") + + pkGenConfig := PrivateKeyGen{ + outputPath: pkOut, + keyBitSize: pkSize, + saltSize: saltSize, + } + + privateKey, err := generatePrivKey(pkGenConfig) + logger.HaltOnErr(err) + + pubOut, _ := cmd.Flags().GetString("pub-out") + err = generatePubKey(pubOut, privateKey) + logger.HaltOnErr(err) + }, +} + +func generatePubKey(path string, privKey *rsa.PrivateKey) error { + absPath, err := filepath.Abs(path) + if err != nil { + return fmt.Errorf("failed to get absolute path: %v", err) + } + + pubASN1, err := x509.MarshalPKIXPublicKey(&privKey.PublicKey) + if err != nil { + return fmt.Errorf("failed to marshal public key: %w", err) + } + + file, err := os.Create(absPath) + if err != nil { + return fmt.Errorf("failed to create public key file: %w", err) + } + defer file.Close() + + if err := pem.Encode(file, &pem.Block{Type: "RSA PUBLIC KEY", Bytes: pubASN1}); err != nil { + return fmt.Errorf("failed to encode public key to PEM: %w", err) + } + + return nil +} + +func generatePrivKey(pkGenConfig PrivateKeyGen) (*rsa.PrivateKey, error) { + absPath, err := filepath.Abs(pkGenConfig.outputPath) + if err != nil { + return nil, fmt.Errorf("failed to get absolute path: %v", err) + } + + privateKey, err := rsa.GenerateKey(rand.Reader, pkGenConfig.keyBitSize) + if err != nil { + return nil, fmt.Errorf("failed to generate private key: %v", err) + } + + passphrase, err := utils.GetPassphrase() + if err != nil { + return nil, err + } + + privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) + + salt, err := makeSalt(pkGenConfig.saltSize) + if err != nil { + return nil, err + } + + key, err := crypto_utils.DeriveKey(crypto_utils.KeyDerivationConfig{ + Passphrase: passphrase, + Salt: salt, + }) + if err != nil { + return nil, err + } + + crypter, err := crypto_utils.MakeCrypter(key) + if err != nil { + return nil, err + } + + nonce, err := crypto_utils.MakeNonce(crypter) + if err != nil { + return nil, err + } + + encryptedData := crypter.Seal(nil, nonce, privateKeyBytes, nil) + + // Create a PEM block with the encrypted data + encryptedPEMBlock := &pem.Block{ + Type: "ENCRYPTED PRIVATE KEY", + Bytes: encryptedData, + Headers: map[string]string{ + "Nonce": base64.StdEncoding.EncodeToString(nonce), + "Salt": base64.StdEncoding.EncodeToString(salt), + "Key-Derivation-Function": "Argon2", + }, + } + + err = savePrivKeyToPEM(absPath, encryptedPEMBlock) + if err != nil { + return nil, err + } + + return privateKey, nil +} + +func savePrivKeyToPEM(absPath string, encryptedPEMBlock *pem.Block) error { + privKeyFile, err := os.Create(absPath) + if err != nil { + return fmt.Errorf("failed to create private key file: %v", err) + } + defer privKeyFile.Close() + + if err := pem.Encode(privKeyFile, encryptedPEMBlock); err != nil { + return fmt.Errorf("failed to encode private key to PEM: %w", err) + } + + return nil +} + +// makeSalt generates a cryptographic salt. +func makeSalt(saltSize int) ([]byte, error) { + salt := make([]byte, saltSize) + if _, err := rand.Read(salt); err != nil { + return nil, fmt.Errorf("failed to generate salt: %v", err) + } + + return salt, nil +} diff --git a/lesson_7/cmd/keys/keys.go b/lesson_7/cmd/keys/keys.go new file mode 100644 index 0000000..832909e --- /dev/null +++ b/lesson_7/cmd/keys/keys.go @@ -0,0 +1,16 @@ +package keys + +import ( + "github.com/spf13/cobra" +) + +var keysCmd = &cobra.Command{ + Use: "keys", + Short: "Manage key pairs.", + Long: `Use subcommands to create public/private key pairs in PEM files.`, +} + +// Init initializes keys commands +func Init(rootCmd *cobra.Command) { + rootCmd.AddCommand(keysCmd) +} diff --git a/lesson_7/cmd/root.go b/lesson_7/cmd/root.go new file mode 100644 index 0000000..ab4b431 --- /dev/null +++ b/lesson_7/cmd/root.go @@ -0,0 +1,13 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +func RootCmd() *cobra.Command { + return &cobra.Command{ + Use: "brave_signer", + Short: "Bravely generate key pairs, sign files, and check signatures.", + Long: `A collection of tools to generate key pairs in PEM files, sign files, and verify signatures.`, + } +} diff --git a/lesson_7/cmd/signatures/signatures.go b/lesson_7/cmd/signatures/signatures.go new file mode 100644 index 0000000..8067b40 --- /dev/null +++ b/lesson_7/cmd/signatures/signatures.go @@ -0,0 +1,16 @@ +package signatures + +import ( + "github.com/spf13/cobra" +) + +var signaturesCmd = &cobra.Command{ + Use: "signatures", + Short: "Create and verify signatures.", + Long: `Use subcommands to create signature (.sig) with private key and verify signature with public key.`, +} + +// Init initializes signatures commands +func Init(rootCmd *cobra.Command) { + rootCmd.AddCommand(signaturesCmd) +} diff --git a/lesson_7/crypto_utils/crypto_utils.go b/lesson_7/crypto_utils/crypto_utils.go new file mode 100644 index 0000000..07805ae --- /dev/null +++ b/lesson_7/crypto_utils/crypto_utils.go @@ -0,0 +1,49 @@ +package crypto_utils + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "fmt" + + "golang.org/x/crypto/argon2" +) + +type KeyDerivationConfig struct { + Passphrase []byte + Salt []byte +} + +// MakeNonce creates a nonce suitable for use with the provided AEAD cipher. +func MakeNonce(crypter cipher.AEAD) ([]byte, error) { + nonce := make([]byte, crypter.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + return nil, fmt.Errorf("failed to generate nonce: %v", err) + } + + return nonce, nil +} + +// MakeCrypter creates a cipher.AEAD from a given key using AES in GCM mode. +func MakeCrypter(key []byte) (cipher.AEAD, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("failed to create cipher block: %v", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("failed to create GCM cipher: %v", err) + } + + return gcm, nil +} + +// DeriveKey generates a cryptographic key using Argon2 from a given passphrase and salt. +func DeriveKey(config KeyDerivationConfig) ([]byte, error) { + if len(config.Passphrase) == 0 || len(config.Salt) == 0 { + return nil, fmt.Errorf("passphrase and salt cannot be empty") + } + + return argon2.IDKey(config.Passphrase, config.Salt, 1, 64*1024, 4, 32), nil +} diff --git a/lesson_7/go.mod b/lesson_7/go.mod new file mode 100644 index 0000000..9cc38cf --- /dev/null +++ b/lesson_7/go.mod @@ -0,0 +1,15 @@ +module lesson7 + +go 1.22.2 + +require ( + github.com/spf13/cobra v1.8.1 + golang.org/x/crypto v0.24.0 + golang.org/x/term v0.21.0 +) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/sys v0.21.0 // indirect +) diff --git a/lesson_7/go.sum b/lesson_7/go.sum new file mode 100644 index 0000000..9f031b4 --- /dev/null +++ b/lesson_7/go.sum @@ -0,0 +1,16 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/lesson_7/logger/logger.go b/lesson_7/logger/logger.go new file mode 100644 index 0000000..7cb4d06 --- /dev/null +++ b/lesson_7/logger/logger.go @@ -0,0 +1,46 @@ +package logger + +import ( + "fmt" + "log" + "os" + "strings" +) + +var ( + // Using standard error for error logs and standard output for info and warning logs. + errorLogger = log.New(os.Stderr, "ERROR: ", log.LstdFlags|log.Lshortfile) + warnLogger = log.New(os.Stdout, "WARN: ", log.LstdFlags) + infoLogger = log.New(os.Stdout, "INFO: ", log.LstdFlags) +) + +// HaltOnErr logs an error and exits if the error is non-nil. +func HaltOnErr(err error, messages ...string) { + if err == nil { + return + } + + message := "An error occurred" + + if len(messages) > 0 { + message = fmt.Sprintf("%s: %s", message, strings.Join(messages, " ")) + } + errorLogger.Printf("%s: %v", message, err) + os.Exit(1) +} + +// Info logs an informational message. +func Info(message string) { + infoLogger.Println(message) +} + +// Warn logs a warning message along with an error if provided. +func Warn(err error, messages ...string) { + if err != nil { + message := "A warning occurred" + if len(messages) > 0 { + message = fmt.Sprintf("%s: %s", message, strings.Join(messages, " ")) + } + warnLogger.Printf("%s: %v", message, err) + } +} diff --git a/lesson_7/main.go b/lesson_7/main.go new file mode 100644 index 0000000..ede032d --- /dev/null +++ b/lesson_7/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "lesson7/cmd" + "lesson7/cmd/keys" + "lesson7/cmd/signatures" + "lesson7/logger" +) + +func main() { + rootCmd := cmd.RootCmd() + keys.Init(rootCmd) + signatures.Init(rootCmd) + + if err := rootCmd.Execute(); err != nil { + logger.HaltOnErr(err, "Initial setup failed") + } +} diff --git a/lesson_7/priv_key.pem b/lesson_7/priv_key.pem new file mode 100644 index 0000000..bf30173 --- /dev/null +++ b/lesson_7/priv_key.pem @@ -0,0 +1,32 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +Key-Derivation-Function: Argon2 +Nonce: luxSG2uq5IhEGn7y +Salt: nOogu0LAy2n+1L5xHBjKOw== + +gJB0xo0QIsPh5nZjPkqoEPURsll/Pq+XkIa44wiraXxn1Rd7g5yk5lZiNvo2AJuZ +gPAqFIIRTeJxpoqL8KkgyYeOnendVULotnvfYWQ42dx4z2Fn9YAa5PSim3EgfzfN +Cawrzf8v5aXbmVp8aH1PEsSELl/Nat6bLI9duvVbiZNRwkMX9eiZWjSHAvtRqGFW +ub3VzfEXaGmdeZheA0LljRIY+uKQXY6/6Ad2eQ+HX0Mca44guljAy+3Y/qtL1+5k +DKiuNe1AxSEhyyAjbw8w53x71VVQdfKnGyICKQsoiIUNsFMnJ1i8BViyXiKh2xLg +9gOivgRtYSBzu2GeW5p5GvuSHJ5bGN/RAOR+d7IVUN4lRPah6VmSaayTttgcA72s +McMNNbMgq+jJSUMbUOlXh58MNXgU0TDllWg56BAG8oLcEjL0jPqS2mcOf985QJLP +R3OjdJ7uxJXC58R8/+VzpZXesJwKWPcB1EQTpdcuh61gEyEQu0V5PtkQkxHF+qu4 +7hsz8EXBQ5IM3JWUKPEuwJbY9LRKr0fPNi/Ia3F7l7rnz9P6vC33rYOFnXj47KcP +Sj7quUaXiA8Z0pwxQ33ZVAiVlTvsivlX3qhhx6qo+FN77RMO7kE1WmWB61Sepo2Y +whog1x0WTWeC5/H4s415VO0NCcPNRKMG+mA0YKnxn/yz6teEFzgTV+3yorlU0xsZ +HigXmSBLXP5Y9Sk7AIEVq7F3RlguknbkGjfnci/NjYWHZThVtUUoLeMoujQhadt3 +WQ52VLH+Jx5IH6M8A0NgDfwZEbjR7XtmGXXBDLSMlD8OtpMk9j7OiwspnW9NkyyY +J3qeyLHIlHaOO5mPxrvr6eiQldr9yyjdyjeT917XEMm5SFJOa0ARD0q0inbFZe13 +OwsLaNCmA81nD94tQkiMIz5CDvvG3q0noU27SxBeUVXDRG00c1Cq+81t8SVJ5hLG +Qb7mFYSHbX9C6f85TLXu/WYV4UNiTP6y1+lLn8gcmpF39uvk+yIjFIlZm1/k4/0z +NXwxZWxPxfvivBy2k4liqUVXKMx8sl3en+YP96QdHXp0rUSYkd2f9p3puot3fRkk +7m0KkEK7cQ1DSPp8fI7h9bk2IFSGP0QrWzyGUnncsxhWzpbO2WMkbEA/eijX1wUL +C6r35/nGf4DlKG5ENmjDbo1Uia+8rxO8Ey6eFH22KtuV4eKXixbsZZIfqeX/hdAS +2BaiyNndFFpnc7j8+8SM+pGZuKx6ioqenQPvxZvmIzpnuwoimywZfYbvqXFFBIUv +vbLRPy6Su/EWOQVMzONfbSo19zal+1Y+LjGAjHw27Hr0V2wtZEmGHeCafzZh9bfL +JzS0j7bAFumjvA1j47MW+XYZSEQDuWWtf8eB7agJXtrBuyIB7unN5U06eB3yuqzK +34+hz9bfYnCE67S9/glkTjzG7QKSuRiYTyQWVihemAaL4ICbfJMakwClOyHE/Ta3 +8Kwu+M5hMUQJPkEjjikfBbQM4AuvgSae9N3WUfay5yyK+FmftsaV/CFAbN3jWm2W +Y8cn6A3NM0K6P2mxCTgKRtw1VLkYbWd/9Jekrrrv99/A4Bv8NPg28vYGdMmEioyU +S+0Q02c+q60q +-----END ENCRYPTED PRIVATE KEY----- diff --git a/lesson_7/pub.pem b/lesson_7/pub.pem new file mode 100644 index 0000000..c4324f3 --- /dev/null +++ b/lesson_7/pub.pem @@ -0,0 +1,9 @@ +-----BEGIN RSA PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4ybssbtHZqJnDlGy4aK9 +OK87GF/IsUiOz5g/wOwP/iWACKCBjyoCxHoPTHKip3OHBeZ7UmAk91zZSrC14Gov +uGGrEHDSc8wZasmEFssBw2HRFqDKnQS+JQhlk94+YAgLivsIhn4cjRD95jLA3W7t +FRYrdNj3Kw16upJXs2tzUEMctTPc/+R1Pr20VReNLs09Cglj5f3t1pHSjj401f1F +ULlrjKzYVBdYRE8sEDYGKMZ6xAu0mkm6Xo13+8sQ3leYHq3y1ED47BsRg7KDpfY5 +CYfteAIzawZUh6Q7cMKksdTgV+CF8fswfoy5lQQOUNSXGZvUqkMD8pYGMKI5CPZQ +kQIDAQAB +-----END RSA PUBLIC KEY----- diff --git a/lesson_7/utils/utils.go b/lesson_7/utils/utils.go new file mode 100644 index 0000000..e3bd7b0 --- /dev/null +++ b/lesson_7/utils/utils.go @@ -0,0 +1,34 @@ +package utils + +import ( + "fmt" + "golang.org/x/term" + "os" + + "lesson7/logger" +) + +// GetPassphrase prompts the user for a passphrase and securely reads it. +func GetPassphrase() ([]byte, error) { + fmt.Println("Enter passphrase:") + + oldState, err := term.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + return nil, fmt.Errorf("failed to set terminal to raw mode: %w", err) + } + defer safeRestore(int(os.Stdin.Fd()), oldState) + + passphrase, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return nil, fmt.Errorf("failed to read passphrase: %w", err) + } + + return passphrase, nil +} + +// safeRestore attempts to restore the terminal to its original state and logs an error if it fails. +func safeRestore(fd int, state *term.State) { + if err := term.Restore(fd, state); err != nil { + logger.HaltOnErr(fmt.Errorf("failed to restore terminal state: %v", err), "Terminal restoration failed") + } +}