-
Notifications
You must be signed in to change notification settings - Fork 91
Expand file tree
/
Copy pathcodec.go
More file actions
148 lines (135 loc) · 3.77 KB
/
Copy pathcodec.go
File metadata and controls
148 lines (135 loc) · 3.77 KB
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
137
138
139
140
141
142
143
144
145
146
147
148
package conversationstenography
import (
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
)
// Envelope is deliberately explicit. It protects message contents without
// pretending ciphertext is ordinary human conversation.
type Envelope struct {
Version int `json:"v"`
Index uint64 `json:"i"`
Nonce string `json:"n"`
Body string `json:"b"`
}
const wirePrefix = "enc:DEC1."
type Encoder struct {
aead cipher.AEAD
chain [32]byte
index uint64
}
type Decoder struct {
aead cipher.AEAD
chain [32]byte
index uint64
}
func derive(key []byte, label string) [32]byte {
h := hmac.New(sha256.New, key)
h.Write([]byte("decalgo-v1:" + label))
var out [32]byte
copy(out[:], h.Sum(nil))
return out
}
func newAEAD(key []byte) (cipher.AEAD, error) {
k := derive(key, "message-key")
block, err := aes.NewCipher(k[:])
if err != nil {
return nil, err
}
return cipher.NewGCM(block)
}
func NewEncoder(key []byte, conversationID string) (*Encoder, error) {
if len(key) < 16 {
return nil, errors.New("key must contain at least 16 bytes of entropy")
}
a, err := newAEAD(key)
if err != nil {
return nil, err
}
return &Encoder{aead: a, chain: derive(key, "conversation:"+conversationID)}, nil
}
func NewDecoder(key []byte, conversationID string) (*Decoder, error) {
if len(key) < 16 {
return nil, errors.New("key must contain at least 16 bytes of entropy")
}
a, err := newAEAD(key)
if err != nil {
return nil, err
}
return &Decoder{aead: a, chain: derive(key, "conversation:"+conversationID)}, nil
}
func associatedData(index uint64, chain [32]byte) []byte {
b := make([]byte, 40)
binary.BigEndian.PutUint64(b[:8], index)
copy(b[8:], chain[:])
return b
}
func advance(chain [32]byte, index uint64, nonce, ciphertext []byte) [32]byte {
h := sha256.New()
h.Write([]byte("decalgo-chain-v1"))
h.Write(chain[:])
var n [8]byte
binary.BigEndian.PutUint64(n[:], index)
h.Write(n[:])
h.Write(nonce)
h.Write(ciphertext)
var out [32]byte
copy(out[:], h.Sum(nil))
return out
}
func (e *Encoder) Encode(plaintext string) (string, error) {
nonce := make([]byte, e.aead.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return "", err
}
ct := e.aead.Seal(nil, nonce, []byte(plaintext), associatedData(e.index, e.chain))
env := Envelope{1, e.index, base64.RawURLEncoding.EncodeToString(nonce), base64.RawURLEncoding.EncodeToString(ct)}
b, err := json.Marshal(env)
if err != nil {
return "", err
}
e.chain = advance(e.chain, e.index, nonce, ct)
e.index++
return wirePrefix + base64.RawURLEncoding.EncodeToString(b), nil
}
func (d *Decoder) Decode(encoded string) (string, error) {
if len(encoded) < len(wirePrefix) || encoded[:len(wirePrefix)] != wirePrefix {
return "", errors.New("not a marked DEC1 envelope")
}
b, err := base64.RawURLEncoding.DecodeString(encoded[len(wirePrefix):])
if err != nil {
return "", fmt.Errorf("invalid envelope: %w", err)
}
var env Envelope
if err := json.Unmarshal(b, &env); err != nil {
return "", fmt.Errorf("invalid envelope: %w", err)
}
if env.Version != 1 {
return "", fmt.Errorf("unsupported version %d", env.Version)
}
if env.Index != d.index {
return "", fmt.Errorf("unexpected message index %d; want %d", env.Index, d.index)
}
nonce, err := base64.RawURLEncoding.DecodeString(env.Nonce)
if err != nil || len(nonce) != d.aead.NonceSize() {
return "", errors.New("invalid nonce")
}
ct, err := base64.RawURLEncoding.DecodeString(env.Body)
if err != nil {
return "", errors.New("invalid ciphertext")
}
pt, err := d.aead.Open(nil, nonce, ct, associatedData(d.index, d.chain))
if err != nil {
return "", errors.New("authentication failed")
}
d.chain = advance(d.chain, d.index, nonce, ct)
d.index++
return string(pt), nil
}