-
Notifications
You must be signed in to change notification settings - Fork 99
/
encryption.go
247 lines (189 loc) · 8.31 KB
/
encryption.go
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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
package lksdk
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"errors"
"io"
"golang.org/x/crypto/hkdf"
"golang.org/x/crypto/pbkdf2"
)
const (
LIVEKIT_SDK_SALT = "LKFrameEncryptionKey"
LIVEKIT_IV_LENGTH = 12
LIVEKIT_PBKDF_ITERATIONS = 100000
LIVEKIT_KEY_SIZE_BYTES = 16
LIVEKIT_HKDF_INFO_BYTES = 128
unencrypted_audio_bytes = 1
)
var (
ErrIncorrectKeyLength = errors.New("incorrect key length for encryption/decryption")
ErrUnableGenerateIV = errors.New("unable to generate iv for encryption")
ErrIncorrectIVLength = errors.New("incorrect iv length")
ErrIncorrectSecretLength = errors.New("input secret provided to derivation function cannot be empty or nil")
ErrIncorrectSaltLength = errors.New("input salt provided to derivation function cannot be empty or nil")
ErrBlockCipherRequired = errors.New("input block cipher cannot be nil")
)
func DeriveKeyFromString(password string) ([]byte, error) {
return DeriveKeyFromStringCustomSalt(password, LIVEKIT_SDK_SALT)
}
func DeriveKeyFromStringCustomSalt(password, salt string) ([]byte, error) {
if password == "" {
return nil, ErrIncorrectSecretLength
}
if salt == "" {
return nil, ErrIncorrectSaltLength
}
encPassword := []byte(password)
encSalt := []byte(salt)
return pbkdf2.Key(encPassword, encSalt, LIVEKIT_PBKDF_ITERATIONS, LIVEKIT_KEY_SIZE_BYTES, sha256.New), nil
}
func DeriveKeyFromBytes(secret []byte) ([]byte, error) {
return DeriveKeyFromBytesCustomSalt(secret, LIVEKIT_SDK_SALT)
}
func DeriveKeyFromBytesCustomSalt(secret []byte, salt string) ([]byte, error) {
info := make([]byte, LIVEKIT_HKDF_INFO_BYTES)
encSalt := []byte(salt)
if secret == nil {
return nil, ErrIncorrectSecretLength
}
if salt == "" {
return nil, ErrIncorrectSaltLength
}
hkdfReader := hkdf.New(sha256.New, secret, encSalt, info)
key := make([]byte, LIVEKIT_KEY_SIZE_BYTES)
_, err := io.ReadFull(hkdfReader, key)
if err != nil {
return nil, err
}
return key, nil
}
// Take audio sample (body of RTP) encrypted by LiveKit client SDK, extract IV and decrypt using provided key
// If sample matches sifTrailer, it's considered to be a non-encrypted Server Injected Frame and nil is returned
// Use DecryptGCMAudioSampleCustomCipher with cached aes cipher block for better (30%) performance
func DecryptGCMAudioSample(sample, key, sifTrailer []byte) ([]byte, error) {
if len(key) != 16 {
return nil, ErrIncorrectKeyLength
}
if sifTrailer != nil && len(sample) >= len(sifTrailer) {
possibleTrailer := sample[len(sample)-len(sifTrailer):]
if bytes.Equal(possibleTrailer, sifTrailer) {
// this is unencrypted Server Injected Frame (SIF) that should be dropped
return nil, nil
}
}
cipherBlock, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return DecryptGCMAudioSampleCustomCipher(sample, sifTrailer, cipherBlock)
}
// Take audio sample (body of RTP) encrypted by LiveKit client SDK, extract IV and decrypt using provided cipherBlock
// If sample matches sifTrailer, it's considered to be a non-encrypted Server Injected Frame and nil is returned
// Encrypted sample format based on livekit client sdk
// ---------+-------------------------+---------+----
// payload |IV...(length = IV_LENGTH)|IV_LENGTH|KID|
// ---------+-------------------------+---------+----
// First byte of audio frame is not encrypted and only authenticated
// payload - variable bytes
// IV - variable bytes (equal to IV_LENGTH bytes)
// IV_LENGTH - 1 byte
// KID (Key ID) - 1 byte - ignored here, key is provided as parameter to function
func DecryptGCMAudioSampleCustomCipher(sample, sifTrailer []byte, cipherBlock cipher.Block) ([]byte, error) {
if cipherBlock == nil {
return nil, ErrBlockCipherRequired
}
if sample == nil {
return nil, nil
}
if sifTrailer != nil && len(sample) >= len(sifTrailer) {
possibleTrailer := sample[len(sample)-len(sifTrailer):]
if bytes.Equal(possibleTrailer, sifTrailer) {
// this is unencrypted Server Injected Frame (SIF) that should be dropped
return nil, nil
}
}
// variable naming is kept close to LiveKit client SDK decrypt function
// https://github.com/livekit/client-sdk-js/blob/main/src/e2ee/worker/FrameCryptor.ts#L402
frameHeader := sample[:unencrypted_audio_bytes] // first unencrypted bytes are "frameHeader" and used for authentication later
frameTrailer := sample[len(sample)-2:] // last 2 bytes having IV_LENGTH and KID (1 byte each)
ivLength := int(frameTrailer[0]) // single byte, Endianness doesn't matter
ivStart := len(sample) - len(frameTrailer) - ivLength
if ivStart < 0 {
return nil, ErrIncorrectIVLength
}
iv := make([]byte, ivLength)
copy(iv, sample[ivStart:ivStart+ivLength]) // copy IV value out of sample into iv
cipherTextStart := len(frameHeader)
cipherTextLength := len(sample) - len(frameTrailer) - ivLength - len(frameHeader)
cipherText := make([]byte, cipherTextLength)
copy(cipherText, sample[cipherTextStart:cipherTextStart+cipherTextLength])
aesGCM, err := cipher.NewGCMWithNonceSize(cipherBlock, ivLength) // standard Nonce size is 12 bytes, but since it MAY be different in the sample, we use the one from the sample
if err != nil {
return nil, err
}
plainText, err := aesGCM.Open(nil, iv, cipherText, frameHeader)
if err != nil {
return nil, err
}
newData := make([]byte, len(frameHeader)+len(plainText)) // allocate space for final packet
_ = copy(newData[0:], frameHeader) // put unencrypted frameHeader first
_ = copy(newData[len(frameHeader):], plainText) // add decrypted remaining value
return newData, nil
}
// Take audio sample (body of RTP) and encrypts it using AES-GCM 128bit with provided key
// Use EncryptGCMAudioSampleCustomCipher with cached aes cipher block for better (20%) performance
func EncryptGCMAudioSample(sample, key []byte, kid uint8) ([]byte, error) {
if len(key) != 16 {
return nil, ErrIncorrectKeyLength
}
cipherBlock, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return EncryptGCMAudioSampleCustomCipher(sample, kid, cipherBlock)
}
// Take audio sample (body of RTP) and encrypts it using AES-GCM 128bit with provided cipher block
// Encrypted sample format based on livekit client sdk
// ---------+-------------------------+---------+----
// payload |IV...(length = IV_LENGTH)|IV_LENGTH|KID|
// ---------+-------------------------+---------+----
// First byte of audio frame is not encrypted and only authenticated
// payload - variable bytes
// IV - variable bytes (equal to IV_LENGTH bytes) - 12 random bytes
// IV_LENGTH - 1 byte - 12 bytes fixed
// KID (Key ID) - 1 byte - taken from "kid" parameter
func EncryptGCMAudioSampleCustomCipher(sample []byte, kid uint8, cipherBlock cipher.Block) ([]byte, error) {
if cipherBlock == nil {
return nil, ErrBlockCipherRequired
}
if sample == nil {
return nil, nil
}
// variable naming is kept close to LiveKit client SDK decrypt function
// https://github.com/livekit/client-sdk-js/blob/main/src/e2ee/worker/FrameCryptor.ts#L402
frameHeader := append(make([]byte, 0), sample[:unencrypted_audio_bytes]...) // first unencrypted bytes are "frameHeader" and used for authentication later
iv := make([]byte, LIVEKIT_IV_LENGTH)
_, err := rand.Read(iv)
if err != nil {
return nil, errors.Join(ErrUnableGenerateIV, err)
}
frameTrailer := []byte{LIVEKIT_IV_LENGTH, kid} // last 2 bytes having IV_LENGTH and KID (1 byte each)
plainTextStart := len(frameHeader)
plainTextLength := len(sample) - len(frameHeader)
plainText := make([]byte, plainTextLength)
copy(plainText, sample[plainTextStart:plainTextStart+plainTextLength])
aesGCM, err := cipher.NewGCMWithNonceSize(cipherBlock, LIVEKIT_IV_LENGTH) // standard Nonce size is 12 bytes, but using one from defined constant (which matches Javascript SDK)
if err != nil {
return nil, err
}
cipherText := aesGCM.Seal(nil, iv, plainText, frameHeader)
newData := make([]byte, len(frameHeader)+len(cipherText)+len(iv)+len(frameTrailer)) // allocate space for final packet
_ = copy(newData[0:], frameHeader) // put unencrypted frameHeader first
_ = copy(newData[len(frameHeader):], cipherText) // add cipherText
_ = copy(newData[len(frameHeader)+len(cipherText):], iv) // add iv
_ = copy(newData[len(frameHeader)+len(cipherText)+len(iv):], frameTrailer) // add trailer
return newData, nil
}