package hamcrypto // Gnarl wire format for authenticated encrypted packets. // 64-byte header (10 nonce + 27 identity + 27 auth tag) + ciphertext. import ( "crypto/sha256" "errors" "golang.org/x/crypto/chacha20" ) const ( GnarlNonceLen = 10 GnarlHeaderLen = GnarlNonceLen + GnarlMidBytes + GnarlMidBytes ) type GnarlPacket struct { Nonce [GnarlNonceLen]byte Identity GnarlMid AuthTag GnarlMid Ciphertext []byte } func GnarlSeal(secret Hamadryad, identity GnarlMid, nonce [GnarlNonceLen]byte, plaintext []byte) (pkt *GnarlPacket) { macKey, cipher := gnarlCipherAndMAC(secret, nonce) ciphertext := []byte{:int32(len(plaintext))} cipher.XORKeyStream(ciphertext, plaintext) authTag := gnarlAuthTag(macKey, nonce, identity, ciphertext) return &GnarlPacket{ Nonce: nonce, Identity: identity, AuthTag: authTag, Ciphertext: ciphertext, } } func GnarlOpen(secret Hamadryad, pkt *GnarlPacket) (plaintext []byte, err error) { if pkt == nil { return nil, errors.New("gnarl: nil packet") } macKey, cipher := gnarlCipherAndMAC(secret, pkt.Nonce) expected := gnarlAuthTag(macKey, pkt.Nonce, pkt.Identity, pkt.Ciphertext) if expected != pkt.AuthTag { return nil, errors.New("gnarl: authentication failed") } plaintext = []byte{:int32(len(pkt.Ciphertext))} cipher.XORKeyStream(plaintext, pkt.Ciphertext) return plaintext, nil } func MarshalGnarlPacket(pkt *GnarlPacket) (buf []byte) { buf = []byte{:int32(GnarlHeaderLen + int32(len(pkt.Ciphertext)))} copy(buf[0:GnarlNonceLen], pkt.Nonce[:]) copy(buf[GnarlNonceLen:GnarlNonceLen+GnarlMidBytes], pkt.Identity[:]) copy(buf[GnarlNonceLen+GnarlMidBytes:GnarlHeaderLen], pkt.AuthTag[:]) copy(buf[GnarlHeaderLen:], pkt.Ciphertext) return buf } func UnmarshalGnarlPacket(data []byte) (pkt *GnarlPacket, err error) { if int32(len(data)) < GnarlHeaderLen { return nil, errors.New("gnarl: packet too short") } pkt = &GnarlPacket{} copy(pkt.Nonce[:], data[0:GnarlNonceLen]) copy(pkt.Identity[:], data[GnarlNonceLen:GnarlNonceLen+GnarlMidBytes]) copy(pkt.AuthTag[:], data[GnarlNonceLen+GnarlMidBytes:GnarlHeaderLen]) pkt.Ciphertext = []byte{:int32(len(data)) - GnarlHeaderLen} copy(pkt.Ciphertext, data[GnarlHeaderLen:]) return pkt, nil } func gnarlCipherAndMAC(secret Hamadryad, nonce [GnarlNonceLen]byte) (macKey Hamadryad, cipher *chacha20.Cipher) { keyHash := Hash([]byte("gnarl-chacha20-key") | secret[:]) var key [32]byte copy(key[:], keyHash[:32]) var chachaNonce [12]byte copy(chachaNonce[:GnarlNonceLen], nonce[:]) cipher, _ = chacha20.NewUnauthenticatedCipher(key[:], chachaNonce[:]) var macBlock [64]byte cipher.XORKeyStream(macBlock[:], macBlock[:]) copy(macKey[:], macBlock[:HamBytes]) return macKey, cipher } func gnarlAuthTag(macKey Hamadryad, nonce [GnarlNonceLen]byte, identity GnarlMid, ciphertext []byte) (tag GnarlMid) { ctHash := GHash(ciphertext) var tagInput []byte tagInput = tagInput | macKey[:] tagInput = tagInput | nonce[:] tagInput = tagInput | identity[:] tagInput = tagInput | ctHash[:] return GMid(tagInput) } // GnarlSchnorrChallenge computes the Fiat-Shamir challenge using SHA-256 // truncated to 27 bytes. func GnarlSchnorrChallenge(data []byte) (result [GnarlMidBytes]byte) { h := sha256.Sum256(data) copy(result[:], h[:GnarlMidBytes]) return result } // GnarlNonceFromCounter creates a nonce from a 64-bit counter and 16-bit epoch. func GnarlNonceFromCounter(epoch uint16, counter uint64) (nonce [GnarlNonceLen]byte) { nonce[0] = byte(counter) nonce[1] = byte(counter >> 8) nonce[2] = byte(counter >> 16) nonce[3] = byte(counter >> 24) nonce[4] = byte(counter >> 32) nonce[5] = byte(counter >> 40) nonce[6] = byte(counter >> 48) nonce[7] = byte(counter >> 56) nonce[8] = byte(epoch) nonce[9] = byte(epoch >> 8) return nonce }