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
|
// Copyright (c) 2024 Sumner Evans
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package backup
import (
"bytes"
"crypto/ecdh"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/json"
"errors"
"go.mau.fi/util/jsonbytes"
"golang.org/x/crypto/hkdf"
"maunium.net/go/mautrix/crypto/aescbc"
)
var ErrInvalidMAC = errors.New("invalid MAC")
// EncryptedSessionData is the encrypted session_data field of a key backup as
// defined in [Section 11.12.3.2.2 of the Spec].
//
// The type parameter T represents the format of the session data contained in
// the encrypted payload.
//
// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
type EncryptedSessionData[T any] struct {
Ciphertext jsonbytes.UnpaddedBytes `json:"ciphertext"`
Ephemeral EphemeralKey `json:"ephemeral"`
MAC jsonbytes.UnpaddedBytes `json:"mac"`
}
func calculateEncryptionParameters(sharedSecret []byte) (key, macKey, iv []byte, err error) {
hkdfReader := hkdf.New(sha256.New, sharedSecret, nil, nil)
encryptionParams := make([]byte, 80)
_, err = hkdfReader.Read(encryptionParams)
if err != nil {
return nil, nil, nil, err
}
return encryptionParams[:32], encryptionParams[32:64], encryptionParams[64:], nil
}
// calculateCompatMAC calculates the MAC as described in step 5 of according to
// [Section 11.12.3.2.2] of the Spec which was updated in spec version 1.10 to
// reflect what is actually implemented in libolm and Vodozemac.
//
// Libolm implemented the MAC functionality incorrectly. The MAC is computed
// over an empty string rather than the ciphertext. Vodozemac implemented this
// functionality the same way as libolm for compatibility. In version 1.10 of
// the spec, the description of step 5 was updated to reflect the de-facto
// standard of libolm and Vodozemac.
//
// [Section 11.12.3.2.2]: https://spec.matrix.org/v1.11/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
func calculateCompatMAC(macKey []byte) []byte {
hash := hmac.New(sha256.New, macKey)
return hash.Sum(nil)[:8]
}
// EncryptSessionData encrypts the given session data with the given recovery
// key as defined in [Section 11.12.3.2.2 of the Spec].
//
// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
func EncryptSessionData[T any](backupKey *MegolmBackupKey, sessionData T) (*EncryptedSessionData[T], error) {
sessionJSON, err := json.Marshal(sessionData)
if err != nil {
return nil, err
}
ephemeralKey, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
sharedSecret, err := ephemeralKey.ECDH(backupKey.PublicKey())
if err != nil {
return nil, err
}
key, macKey, iv, err := calculateEncryptionParameters(sharedSecret)
if err != nil {
return nil, err
}
ciphertext, err := aescbc.Encrypt(key, iv, sessionJSON)
if err != nil {
return nil, err
}
return &EncryptedSessionData[T]{
Ciphertext: ciphertext,
Ephemeral: EphemeralKey{ephemeralKey.PublicKey()},
MAC: calculateCompatMAC(macKey),
}, nil
}
// Decrypt decrypts the [EncryptedSessionData] into a *T using the recovery key
// by reversing the process described in [Section 11.12.3.2.2 of the Spec].
//
// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
func (esd *EncryptedSessionData[T]) Decrypt(backupKey *MegolmBackupKey) (*T, error) {
sharedSecret, err := backupKey.ECDH(esd.Ephemeral.PublicKey)
if err != nil {
return nil, err
}
key, macKey, iv, err := calculateEncryptionParameters(sharedSecret)
if err != nil {
return nil, err
}
// Verify the MAC before decrypting.
if !bytes.Equal(calculateCompatMAC(macKey), esd.MAC) {
return nil, ErrInvalidMAC
}
plaintext, err := aescbc.Decrypt(key, iv, esd.Ciphertext)
if err != nil {
return nil, err
}
var sessionData T
err = json.Unmarshal(plaintext, &sessionData)
return &sessionData, err
}
|