/
keyexport.go
189 lines (159 loc) · 5.56 KB
/
keyexport.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
// Copyright (c) 2020 Tulir Asokan
//
// 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 crypto
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"math"
"golang.org/x/crypto/pbkdf2"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/id"
)
type SenderClaimedKeys struct {
Ed25519 id.Ed25519 `json:"ed25519"`
}
type ExportedSession struct {
Algorithm id.Algorithm `json:"algorithm"`
ForwardingChains []string `json:"forwarding_curve25519_key_chain"`
RoomID id.RoomID `json:"room_id"`
SenderKey id.SenderKey `json:"sender_key"`
SenderClaimedKeys SenderClaimedKeys `json:"sender_claimed_keys"`
SessionID id.SessionID `json:"session_id"`
SessionKey string `json:"session_key"`
}
// The default number of pbkdf2 rounds to use when exporting keys
const defaultPassphraseRounds = 100000
const exportPrefix = "-----BEGIN MEGOLM SESSION DATA-----\n"
const exportSuffix = "-----END MEGOLM SESSION DATA-----\n"
// Only version 0x01 is currently specified in the spec
const exportVersion1 = 0x01
// The standard for wrapping base64 is 76 bytes
const exportLineLengthLimit = 76
// Byte count for version + salt + iv + number of rounds
const exportHeaderLength = 1 + 16 + 16 + 4
// SHA-256 hash length
const exportHashLength = 32
func computeKey(passphrase string, salt []byte, rounds int) (encryptionKey, hashKey []byte) {
key := pbkdf2.Key([]byte(passphrase), salt, rounds, 64, sha512.New)
encryptionKey = key[:32]
hashKey = key[32:]
return
}
func makeExportIV() []byte {
iv := make([]byte, 16)
_, err := rand.Read(iv)
if err != nil {
panic(olm.NotEnoughGoRandom)
}
// Set bit 63 to zero
iv[7] &= 0b11111110
return iv
}
func makeExportKeys(passphrase string) (encryptionKey, hashKey, salt, iv []byte) {
salt = make([]byte, 16)
_, err := rand.Read(salt)
if err != nil {
panic(olm.NotEnoughGoRandom)
}
encryptionKey, hashKey = computeKey(passphrase, salt, defaultPassphraseRounds)
iv = makeExportIV()
return
}
func exportSessions(sessions []*InboundGroupSession) ([]ExportedSession, error) {
export := make([]ExportedSession, len(sessions))
for i, session := range sessions {
key, err := session.Internal.Export(session.Internal.FirstKnownIndex())
if err != nil {
return nil, fmt.Errorf("failed to export session: %w", err)
}
export[i] = ExportedSession{
Algorithm: id.AlgorithmMegolmV1,
ForwardingChains: session.ForwardingChains,
RoomID: session.RoomID,
SenderKey: session.SenderKey,
SenderClaimedKeys: SenderClaimedKeys{},
SessionID: session.ID(),
SessionKey: key,
}
}
return export, nil
}
func exportSessionsJSON(sessions []*InboundGroupSession) ([]byte, error) {
exportedSessions, err := exportSessions(sessions)
if err != nil {
return nil, err
}
return json.Marshal(exportedSessions)
}
func min(a, b int) int {
if a > b {
return b
}
return a
}
func formatKeyExportData(data []byte) []byte {
base64Data := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
base64.StdEncoding.Encode(base64Data, data)
// Prefix + data and newline for each 76 characters of data + suffix
outputLength := len(exportPrefix) +
len(base64Data) + int(math.Ceil(float64(len(base64Data))/exportLineLengthLimit)) +
len(exportSuffix)
var buf bytes.Buffer
buf.Grow(outputLength)
buf.WriteString(exportPrefix)
for ptr := 0; ptr < len(base64Data); ptr += exportLineLengthLimit {
buf.Write(base64Data[ptr:min(ptr+exportLineLengthLimit, len(base64Data))])
buf.WriteRune('\n')
}
buf.WriteString(exportSuffix)
if buf.Len() != buf.Cap() || buf.Len() != outputLength {
panic(fmt.Errorf("unexpected length %d / %d / %d", buf.Len(), buf.Cap(), outputLength))
}
return buf.Bytes()
}
// ExportKeys exports the given Megolm sessions with the format specified in the Matrix spec.
// See https://spec.matrix.org/v1.2/client-server-api/#key-exports
func ExportKeys(passphrase string, sessions []*InboundGroupSession) ([]byte, error) {
// Make all the keys necessary for exporting
encryptionKey, hashKey, salt, iv := makeExportKeys(passphrase)
// Export all the given sessions and put them in JSON
unencryptedData, err := exportSessionsJSON(sessions)
if err != nil {
return nil, err
}
// The export data consists of:
// 1 byte of export format version
// 16 bytes of salt
// 16 bytes of IV (initialization vector)
// 4 bytes of the number of rounds
// the encrypted export data
// 32 bytes of the hash of all the data above
exportData := make([]byte, exportHeaderLength+len(unencryptedData)+exportHashLength)
dataWithoutHashLength := len(exportData) - exportHashLength
// Create the header for the export data
exportData[0] = exportVersion1
copy(exportData[1:17], salt)
copy(exportData[17:33], iv)
binary.BigEndian.PutUint32(exportData[33:37], defaultPassphraseRounds)
// Encrypt data with AES-256-CTR
block, _ := aes.NewCipher(encryptionKey)
cipher.NewCTR(block, iv).XORKeyStream(exportData[exportHeaderLength:dataWithoutHashLength], unencryptedData)
// Hash all the data with HMAC-SHA256 and put it at the end
mac := hmac.New(sha256.New, hashKey)
mac.Write(exportData[:dataWithoutHashLength])
mac.Sum(exportData[:dataWithoutHashLength])
// Format the export (prefix, base64'd exportData, suffix) and return
return formatKeyExportData(exportData), nil
}