package kate import ( "crypto/rand" "encoding/hex" "errors" "fmt" "golang.org/x/crypto/poly1305" //nolint:staticcheck "golang.org/x/crypto/salsa20" ) type encryption struct { Key [naclKeyLen]byte } var ErrInvalidToken = errors.New("invalid token") func encryptionFromHexKey(key string) (encryption, error) { e := encryption{} l := hex.EncodedLen(naclKeyLen) keybytes := []byte(key) if l != len(keybytes) { return e, fmt.Errorf("expected %d length encryption key, got %d", l, len(keybytes)) } if _, err := hex.Decode(e.Key[:], keybytes); err != nil { return e, fmt.Errorf("encryptionFromHexKey: %w", err) } return e, nil } func (e encryption) Encrypt(data []byte) string { buf := make([]byte, naclNonceLen+len(data)+naclMacLen) nonce := [naclNonceLen]byte{} _, _ = rand.Read(nonce[:]) seal(buf[naclNonceLen:], e.Key, nonce, data) copy(buf[:naclNonceLen], nonce[:]) return hex.EncodeToString(buf) } func (e encryption) Decrypt(token string) ([]byte, bool) { tokenBytes := []byte(token) ciphertext := make([]byte, hex.DecodedLen(len(tokenBytes))) if _, err := hex.Decode(ciphertext, tokenBytes); err != nil { return nil, false } nonce := ciphertext[:naclNonceLen] noncebuf := [naclNonceLen]byte{} copy(noncebuf[:], nonce) ciphertext = ciphertext[naclNonceLen:] cleartext := make([]byte, len(ciphertext)-naclMacLen) if _, ok := open(cleartext, e.Key, noncebuf, ciphertext); !ok { return nil, false } return cleartext, true } const ( naclKeyLen = 32 naclNonceLen = 24 naclMacLen = 16 ) // // Replicating the libsodium secretbox API below. // // This was a last resort. I really didn't want to introduce CGO, // and the only pure-go implementation I could find (libgodium) // is unmaintained and riddled with bugs. // // With solid go implementations of the core primitives already in // golang.org/x/crypto, it made the most sense to just wrap those. // func seal(dst []byte, key [naclKeyLen]byte, nonce [naclNonceLen]byte, plaintext []byte) []byte { if len(dst) < len(plaintext)+naclMacLen { dst = make([]byte, len(plaintext)+naclMacLen) } keystream := make([]byte, naclKeyLen+len(plaintext)) salsa20.XORKeyStream(keystream, keystream, nonce[:], &key) var poly1305Key [naclKeyLen]byte copy(poly1305Key[:], keystream[:naclKeyLen]) ciphertext := make([]byte, len(plaintext)) for i := range plaintext { ciphertext[i] = plaintext[i] ^ keystream[naclKeyLen+i] } var mac [naclMacLen]byte poly1305.Sum(&mac, ciphertext, &poly1305Key) copy(dst[:naclMacLen], mac[:]) copy(dst[naclMacLen:], ciphertext) return dst } func open(dst []byte, key [naclKeyLen]byte, nonce [naclNonceLen]byte, box []byte) ([]byte, bool) { if len(box) < naclMacLen { return nil, false } if len(dst) < len(box)-naclMacLen { dst = make([]byte, len(box)-naclMacLen) } var receivedMAC [naclMacLen]byte copy(receivedMAC[:], box[:naclMacLen]) ciphertext := box[naclMacLen:] keystream := make([]byte, naclKeyLen+len(ciphertext)) salsa20.XORKeyStream(keystream, keystream, nonce[:], &key) var poly1305Key [naclKeyLen]byte copy(poly1305Key[:], keystream[:naclKeyLen]) var expectedMAC [naclMacLen]byte poly1305.Sum(&expectedMAC, ciphertext, &poly1305Key) if !poly1305.Verify(&receivedMAC, ciphertext, &poly1305Key) { return nil, false } for i := range ciphertext { dst[i] = ciphertext[i] ^ keystream[naclKeyLen+i] } return dst, true }