summaryrefslogtreecommitdiff
path: root/encryption.go
blob: a445668811d6f9d6cad690c9fa9b6fa0fd643254 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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
}