diff --git a/himbocrypt.go b/himbocrypt.go new file mode 100644 index 0000000..ecd4101 --- /dev/null +++ b/himbocrypt.go @@ -0,0 +1,12 @@ +package himbocrypt + +import "himbocrypt/pkg/engine" + +type ( + Engine = engine.Engine + KeyPair = engine.KeyPair +) + +func NewEngine() *Engine { + return engine.NewEngine() +} diff --git a/pkg/engine/constants.go b/pkg/engine/constants.go new file mode 100644 index 0000000..d860b71 --- /dev/null +++ b/pkg/engine/constants.go @@ -0,0 +1,17 @@ +package engine + +import "golang.org/x/crypto/chacha20poly1305" + +const ( + // PubKeySize is the size of an X25519 public key in bytes + PubKeySize = 32 + + // NonceSize is the size of an XChaCha20-Poly1305 nonce + NonceSize = chacha20poly1305.NonceSizeX + + // KeySize is the size of the symmetric key for XChaCha20-Poly1305 + KeySize = chacha20poly1305.KeySize + + // HKDFInfo is the context string for key derivation + HKDFInfo = "HIMBOCRYPT_V1" +) diff --git a/pkg/engine/decrypt.go b/pkg/engine/decrypt.go new file mode 100644 index 0000000..e794780 --- /dev/null +++ b/pkg/engine/decrypt.go @@ -0,0 +1,52 @@ +package engine + +import ( + "crypto/ecdh" + "errors" + "fmt" + + "golang.org/x/crypto/chacha20poly1305" +) + +func (e *Engine) Decrypt(recipientPriv *ecdh.PrivateKey, senderPub *ecdh.PublicKey, encryptedMsg []byte) ([]byte, error) { + 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 := e.curve.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 + symmetricKey, err := deriveKey(ss1, ss2, nonce) + if err != nil { + return nil, err + } + + aead, err := chacha20poly1305.NewX(symmetricKey) + if err != nil { + return nil, fmt.Errorf("failed to create aead: %w", err) + } + + // Bind ephemeral and sender public keys to ciphertext via AAD + aad := buildAAD(ephemeralPubBytes, senderPub.Bytes()) + + return aead.Open(nil, nonce, ciphertext, aad) +} diff --git a/pkg/engine/encrypt.go b/pkg/engine/encrypt.go new file mode 100644 index 0000000..b45751a --- /dev/null +++ b/pkg/engine/encrypt.go @@ -0,0 +1,64 @@ +package engine + +import ( + "crypto/ecdh" + "crypto/rand" + "fmt" + "io" + + "golang.org/x/crypto/chacha20poly1305" +) + +// 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 := e.curve.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, NonceSize) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, fmt.Errorf("failed to generate nonce: %w", err) + } + + // Derive key from both secrets + symmetricKey, err := deriveKey(ss1, ss2, nonce) + if err != nil { + return nil, err + } + + aead, err := chacha20poly1305.NewX(symmetricKey) + if err != nil { + return nil, fmt.Errorf("failed to create aead: %w", err) + } + + // Pack it all up + ephemeralPubBytes := ephemeralPub.Bytes() + senderPubBytes := senderPriv.PublicKey().Bytes() + + // Bind ephemeral and sender public keys to ciphertext via AAD + aad := buildAAD(ephemeralPubBytes, senderPubBytes) + + dst := make([]byte, 0, len(ephemeralPubBytes)+len(nonce)+len(plaintext)+aead.Overhead()) + dst = append(dst, ephemeralPubBytes...) + dst = append(dst, nonce...) + return aead.Seal(dst, nonce, plaintext, aad), nil +} diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 127a38a..16609d9 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -1,32 +1,23 @@ package engine import ( -"crypto/ecdh" -"crypto/rand" -"crypto/sha256" -"encoding/hex" -"errors" -"fmt" -"io" - -"golang.org/x/crypto/chacha20poly1305" -"golang.org/x/crypto/hkdf" + "crypto/ecdh" + "crypto/rand" + "fmt" ) -type KeyPair struct { - PrivateKey *ecdh.PrivateKey - PublicKey *ecdh.PublicKey +type Engine struct { + curve ecdh.Curve } -type Engine struct{} - func NewEngine() *Engine { - return &Engine{} + return &Engine{ + curve: ecdh.X25519(), + } } func (e *Engine) GenerateKeyPair() (*KeyPair, error) { - curve := ecdh.X25519() - priv, err := curve.GenerateKey(rand.Reader) + priv, err := e.curve.GenerateKey(rand.Reader) if err != nil { return nil, fmt.Errorf("failed to generate private key: %w", err) } @@ -35,122 +26,3 @@ func (e *Engine) GenerateKeyPair() (*KeyPair, error) { 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) -} diff --git a/pkg/engine/kdf.go b/pkg/engine/kdf.go new file mode 100644 index 0000000..e3fcd15 --- /dev/null +++ b/pkg/engine/kdf.go @@ -0,0 +1,28 @@ +package engine + +import ( + "crypto/sha256" + "fmt" + "io" + + "golang.org/x/crypto/hkdf" +) + +// deriveKey derives a symmetric key from two shared secrets using HKDF-SHA256 +func deriveKey(ss1, ss2, salt []byte) ([]byte, error) { + ikm := append(ss1, ss2...) + kdf := hkdf.New(sha256.New, ikm, salt, []byte(HKDFInfo)) + symmetricKey := make([]byte, KeySize) + if _, err := io.ReadFull(kdf, symmetricKey); err != nil { + return nil, fmt.Errorf("key derivation failed: %w", err) + } + return symmetricKey, nil +} + +// buildAAD constructs the associated authenticated data from public keys +func buildAAD(ephemeralPubBytes, senderPubBytes []byte) []byte { + aad := make([]byte, 0, len(ephemeralPubBytes)+len(senderPubBytes)) + aad = append(aad, ephemeralPubBytes...) + aad = append(aad, senderPubBytes...) + return aad +} diff --git a/pkg/engine/keys.go b/pkg/engine/keys.go new file mode 100644 index 0000000..46d235d --- /dev/null +++ b/pkg/engine/keys.go @@ -0,0 +1,35 @@ +package engine + +import ( + "crypto/ecdh" + "encoding/hex" +) + +type KeyPair struct { + PrivateKey *ecdh.PrivateKey + PublicKey *ecdh.PublicKey +} + +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 e.curve.NewPrivateKey(bytes) +} + +func (e *Engine) DecodePublicKey(hexKey string) (*ecdh.PublicKey, error) { + bytes, err := hex.DecodeString(hexKey) + if err != nil { + return nil, err + } + return e.curve.NewPublicKey(bytes) +}