First run of HimboCrypt
This commit is contained in:
60
README.md
Normal file
60
README.md
Normal file
@@ -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 <YOUR_PRIVATE_KEY> \
|
||||||
|
-recipient-pub <RECIPIENT_PUBLIC_KEY> \
|
||||||
|
-msg "Hello there"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Read a Message
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./himbocrypt decrypt \
|
||||||
|
-recipient-priv <YOUR_PRIVATE_KEY> \
|
||||||
|
-sender-pub <SENDER_PUBLIC_KEY> \
|
||||||
|
-ciphertext <THE_CIPHERTEXT>
|
||||||
|
```
|
||||||
|
|
||||||
|
## For Developers
|
||||||
|
|
||||||
|
Use this in your own Go apps:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "himbocrypt/pkg/engine"
|
||||||
|
|
||||||
|
e := engine.NewEngine()
|
||||||
|
```
|
||||||
125
cmd/himbocrypt/main.go
Normal file
125
cmd/himbocrypt/main.go
Normal file
@@ -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 <key> -recipient-pub <key> -msg <message>")
|
||||||
|
fmt.Println(" himbocrypt decrypt -recipient-priv <key> -sender-pub <key> -ciphertext <hex>")
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
7
go.mod
Normal file
7
go.mod
Normal file
@@ -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
|
||||||
4
go.sum
Normal file
4
go.sum
Normal file
@@ -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=
|
||||||
BIN
himbocrypt
Executable file
BIN
himbocrypt
Executable file
Binary file not shown.
156
pkg/engine/engine.go
Normal file
156
pkg/engine/engine.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user