commit 2284c4d5625d60da750a4b0c607d3ee8f3c77d77 Author: Atridad Lahiji Date: Thu Dec 11 14:53:01 2025 -0700 First run of HimboCrypt diff --git a/README.md b/README.md new file mode 100644 index 0000000..a6320c0 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# himbocrypt + +A robust end-to-end encryption engine and CLI. + +## What is this? + +An encryption tool for secure message exchange. Uses X25519 key exchange, XChaCha20-Poly1305 for authenticated encryption, and HKDF for key derivation. + +## How it works + +1. **Keys**: You get a "Public Key" (share with the other party) and a "Private Key" (keep secret). +2. **Sending**: The tool combines your private key with the recipient's public key to derive a shared secret and encrypt the message. +3. **Receiving**: The recipient uses their private key and your public key to derive the same secret and decrypt. +4. **Forward Secrecy**: Every message generates an ephemeral keypair that's discarded after use. If your long-term key is compromised later, past messages remain secure. + +## Usage + +### Build + +```bash +go build -o himbocrypt cmd/himbocrypt/main.go +``` + +### Make Keys + +Both parties need to generate their own keypair. + +```bash +./himbocrypt keygen +``` + +Save the output! + +### Send a Message + +```bash +./himbocrypt encrypt \ + -sender-priv \ + -recipient-pub \ + -msg "Hello there" +``` + +### Read a Message + +```bash +./himbocrypt decrypt \ + -recipient-priv \ + -sender-pub \ + -ciphertext +``` + +## For Developers + +Use this in your own Go apps: + +```go +import "himbocrypt/pkg/engine" + +e := engine.NewEngine() +``` diff --git a/cmd/himbocrypt/main.go b/cmd/himbocrypt/main.go new file mode 100644 index 0000000..b19108c --- /dev/null +++ b/cmd/himbocrypt/main.go @@ -0,0 +1,125 @@ +package main + +import ( +"encoding/hex" +"flag" +"fmt" +"os" + +"himbocrypt/pkg/engine" +) + +func main() { + if len(os.Args) < 2 { + printUsage() + os.Exit(1) + } + + command := os.Args[1] + e := engine.NewEngine() + + switch command { + case "keygen": + handleKeygen(e) + case "encrypt": + handleEncrypt(e) + case "decrypt": + handleDecrypt(e) + default: + fmt.Printf("Unknown command: %s\n", command) + printUsage() + os.Exit(1) + } +} + +func printUsage() { + fmt.Println("Usage:") + fmt.Println(" himbocrypt keygen") + fmt.Println(" himbocrypt encrypt -sender-priv -recipient-pub -msg ") + fmt.Println(" himbocrypt decrypt -recipient-priv -sender-pub -ciphertext ") +} + +func handleKeygen(e *engine.Engine) { + kp, err := e.GenerateKeyPair() + if err != nil { + fmt.Fprintf(os.Stderr, "Error generating keys: %v\n", err) + os.Exit(1) + } + fmt.Println("Generated KeyPair:") + fmt.Printf("Private Key: %s\n", kp.EncodePrivateKey()) + fmt.Printf("Public Key: %s\n", kp.EncodePublicKey()) +} + +func handleEncrypt(e *engine.Engine) { + encryptCmd := flag.NewFlagSet("encrypt", flag.ExitOnError) + senderPrivHex := encryptCmd.String("sender-priv", "", "Sender's Private Key (Hex)") + recipientPubHex := encryptCmd.String("recipient-pub", "", "Recipient's Public Key (Hex)") + message := encryptCmd.String("msg", "", "Message to encrypt") + + encryptCmd.Parse(os.Args[2:]) + + if *senderPrivHex == "" || *recipientPubHex == "" || *message == "" { + encryptCmd.Usage() + os.Exit(1) + } + + senderPriv, err := e.DecodePrivateKey(*senderPrivHex) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid sender private key: %v\n", err) + os.Exit(1) + } + + recipientPub, err := e.DecodePublicKey(*recipientPubHex) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid recipient public key: %v\n", err) + os.Exit(1) + } + + ciphertext, err := e.Encrypt(senderPriv, recipientPub, []byte(*message)) + if err != nil { + fmt.Fprintf(os.Stderr, "Encryption failed: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Ciphertext: %s\n", hex.EncodeToString(ciphertext)) +} + +func handleDecrypt(e *engine.Engine) { + decryptCmd := flag.NewFlagSet("decrypt", flag.ExitOnError) + recipientPrivHex := decryptCmd.String("recipient-priv", "", "Recipient's Private Key (Hex)") + senderPubHex := decryptCmd.String("sender-pub", "", "Sender's Public Key (Hex)") + ciphertextHex := decryptCmd.String("ciphertext", "", "Ciphertext (Hex)") + + decryptCmd.Parse(os.Args[2:]) + + if *recipientPrivHex == "" || *senderPubHex == "" || *ciphertextHex == "" { + decryptCmd.Usage() + os.Exit(1) + } + + recipientPriv, err := e.DecodePrivateKey(*recipientPrivHex) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid recipient private key: %v\n", err) + os.Exit(1) + } + + senderPub, err := e.DecodePublicKey(*senderPubHex) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid sender public key: %v\n", err) + os.Exit(1) + } + + ciphertextBytes, err := hex.DecodeString(*ciphertextHex) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid ciphertext hex: %v\n", err) + os.Exit(1) + } + + plaintext, err := e.Decrypt(recipientPriv, senderPub, ciphertextBytes) + if err != nil { + fmt.Fprintf(os.Stderr, "Decryption failed: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Plaintext: %s\n", string(plaintext)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5ee0963 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module himbocrypt + +go 1.24.3 + +require golang.org/x/crypto v0.46.0 + +require golang.org/x/sys v0.39.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..15e020b --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= diff --git a/himbocrypt b/himbocrypt new file mode 100755 index 0000000..6d5a9b1 Binary files /dev/null and b/himbocrypt differ diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go new file mode 100644 index 0000000..127a38a --- /dev/null +++ b/pkg/engine/engine.go @@ -0,0 +1,156 @@ +package engine + +import ( +"crypto/ecdh" +"crypto/rand" +"crypto/sha256" +"encoding/hex" +"errors" +"fmt" +"io" + +"golang.org/x/crypto/chacha20poly1305" +"golang.org/x/crypto/hkdf" +) + +type KeyPair struct { + PrivateKey *ecdh.PrivateKey + PublicKey *ecdh.PublicKey +} + +type Engine struct{} + +func NewEngine() *Engine { + return &Engine{} +} + +func (e *Engine) GenerateKeyPair() (*KeyPair, error) { + curve := ecdh.X25519() + priv, err := curve.GenerateKey(rand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to generate private key: %w", err) + } + return &KeyPair{ + PrivateKey: priv, + PublicKey: priv.PublicKey(), + }, nil +} + +func (kp *KeyPair) EncodePublicKey() string { + return hex.EncodeToString(kp.PublicKey.Bytes()) +} + +func (kp *KeyPair) EncodePrivateKey() string { + return hex.EncodeToString(kp.PrivateKey.Bytes()) +} + +func (e *Engine) DecodePrivateKey(hexKey string) (*ecdh.PrivateKey, error) { + bytes, err := hex.DecodeString(hexKey) + if err != nil { + return nil, err + } + return ecdh.X25519().NewPrivateKey(bytes) +} + +func (e *Engine) DecodePublicKey(hexKey string) (*ecdh.PublicKey, error) { + bytes, err := hex.DecodeString(hexKey) + if err != nil { + return nil, err + } + return ecdh.X25519().NewPublicKey(bytes) +} + +// Encrypt uses a hybrid scheme: +// 1. Ephemeral-Static ECDH (Forward Secrecy) +// 2. Static-Static ECDH (Auth) +// 3. XChaCha20-Poly1305 (AEAD) +// Output: [EphemeralPub] [Nonce] [Ciphertext] +func (e *Engine) Encrypt(senderPriv *ecdh.PrivateKey, recipientPub *ecdh.PublicKey, plaintext []byte) ([]byte, error) { + // Generate ephemeral keys for forward secrecy + ephemeralPriv, err := ecdh.X25519().GenerateKey(rand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to generate ephemeral key: %w", err) + } + ephemeralPub := ephemeralPriv.PublicKey() + + // Calculate shared secrets + ss1, err := ephemeralPriv.ECDH(recipientPub) + if err != nil { + return nil, fmt.Errorf("ecdh ss1 failed: %w", err) + } + + ss2, err := senderPriv.ECDH(recipientPub) + if err != nil { + return nil, fmt.Errorf("ecdh ss2 failed: %w", err) + } + + // Create nonce + nonce := make([]byte, chacha20poly1305.NonceSizeX) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, fmt.Errorf("failed to generate nonce: %w", err) + } + + // Derive key from both secrets + ikm := append(ss1, ss2...) + kdf := hkdf.New(sha256.New, ikm, nonce, []byte("HIMBOCRYPT_V1")) + symmetricKey := make([]byte, chacha20poly1305.KeySize) + if _, err := io.ReadFull(kdf, symmetricKey); err != nil { + return nil, fmt.Errorf("key derivation failed: %w", err) + } + + aead, err := chacha20poly1305.NewX(symmetricKey) + if err != nil { + return nil, fmt.Errorf("failed to create aead: %w", err) + } + + // Pack it all up + dst := make([]byte, 0, len(ephemeralPub.Bytes())+len(nonce)+len(plaintext)+aead.Overhead()) + dst = append(dst, ephemeralPub.Bytes()...) + dst = append(dst, nonce...) + return aead.Seal(dst, nonce, plaintext, nil), nil +} + +func (e *Engine) Decrypt(recipientPriv *ecdh.PrivateKey, senderPub *ecdh.PublicKey, encryptedMsg []byte) ([]byte, error) { + const pubKeySize = 32 + const nonceSize = 24 + + if len(encryptedMsg) < pubKeySize+nonceSize { + return nil, errors.New("message too short") + } + + // Unpack + ephemeralPubBytes := encryptedMsg[:pubKeySize] + nonce := encryptedMsg[pubKeySize : pubKeySize+nonceSize] + ciphertext := encryptedMsg[pubKeySize+nonceSize:] + + ephemeralPub, err := ecdh.X25519().NewPublicKey(ephemeralPubBytes) + if err != nil { + return nil, fmt.Errorf("invalid ephemeral public key: %w", err) + } + + // Reconstruct secrets + ss1, err := recipientPriv.ECDH(ephemeralPub) + if err != nil { + return nil, fmt.Errorf("ecdh ss1 failed: %w", err) + } + + ss2, err := recipientPriv.ECDH(senderPub) + if err != nil { + return nil, fmt.Errorf("ecdh ss2 failed: %w", err) + } + + // Derive key + ikm := append(ss1, ss2...) + kdf := hkdf.New(sha256.New, ikm, nonce, []byte("HIMBOCRYPT_V1")) + symmetricKey := make([]byte, chacha20poly1305.KeySize) + if _, err := io.ReadFull(kdf, symmetricKey); err != nil { + return nil, fmt.Errorf("key derivation failed: %w", err) + } + + aead, err := chacha20poly1305.NewX(symmetricKey) + if err != nil { + return nil, fmt.Errorf("failed to create aead: %w", err) + } + + return aead.Open(nil, nonce, ciphertext, nil) +}