/
keyimport.go
161 lines (142 loc) · 5.56 KB
/
keyimport.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
// Copyright (c) 2024 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"
"context"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"time"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/id"
)
var (
ErrMissingExportPrefix = errors.New("invalid Matrix key export: missing prefix")
ErrMissingExportSuffix = errors.New("invalid Matrix key export: missing suffix")
ErrUnsupportedExportVersion = errors.New("unsupported Matrix key export format version")
ErrMismatchingExportHash = errors.New("mismatching hash; incorrect passphrase?")
ErrInvalidExportedAlgorithm = errors.New("session has unknown algorithm")
ErrMismatchingExportedSessionID = errors.New("imported session has different ID than expected")
)
var exportPrefixBytes, exportSuffixBytes = []byte(exportPrefix), []byte(exportSuffix)
func decodeKeyExport(data []byte) ([]byte, error) {
// If the valid prefix and suffix aren't there, it's probably not a Matrix key export
if !bytes.HasPrefix(data, exportPrefixBytes) {
return nil, ErrMissingExportPrefix
} else if !bytes.HasSuffix(data, exportSuffixBytes) {
return nil, ErrMissingExportSuffix
}
// Remove the prefix and suffix, we don't care about them anymore
data = data[len(exportPrefix) : len(data)-len(exportSuffix)]
// Allocate space for the decoded data. Ignore newlines when counting the length
exportData := make([]byte, base64.StdEncoding.DecodedLen(len(data)-bytes.Count(data, []byte{'\n'})))
n, err := base64.StdEncoding.Decode(exportData, data)
if err != nil {
return nil, err
}
return exportData[:n], nil
}
func decryptKeyExport(passphrase string, exportData []byte) ([]ExportedSession, error) {
if exportData[0] != exportVersion1 {
return nil, ErrUnsupportedExportVersion
}
// Get all the different parts of the export
salt := exportData[1:17]
iv := exportData[17:33]
passphraseRounds := binary.BigEndian.Uint32(exportData[33:37])
dataWithoutHashLength := len(exportData) - exportHashLength
encryptedData := exportData[exportHeaderLength:dataWithoutHashLength]
hash := exportData[dataWithoutHashLength:]
// Compute the encryption and hash keys from the passphrase and salt
encryptionKey, hashKey := computeKey(passphrase, salt, int(passphraseRounds))
// Compute and verify the hash. If it doesn't match, the passphrase is probably wrong
mac := hmac.New(sha256.New, hashKey)
mac.Write(exportData[:dataWithoutHashLength])
if !bytes.Equal(hash, mac.Sum(nil)) {
return nil, ErrMismatchingExportHash
}
// Decrypt the export
block, _ := aes.NewCipher(encryptionKey)
unencryptedData := make([]byte, len(exportData)-exportHashLength-exportHeaderLength)
cipher.NewCTR(block, iv).XORKeyStream(unencryptedData, encryptedData)
// Parse the decrypted JSON
var sessionsJSON []ExportedSession
err := json.Unmarshal(unencryptedData, &sessionsJSON)
if err != nil {
return nil, fmt.Errorf("invalid export json: %w", err)
}
return sessionsJSON, nil
}
func (mach *OlmMachine) importExportedRoomKey(ctx context.Context, session ExportedSession) (bool, error) {
if session.Algorithm != id.AlgorithmMegolmV1 {
return false, ErrInvalidExportedAlgorithm
}
igsInternal, err := olm.InboundGroupSessionImport([]byte(session.SessionKey))
if err != nil {
return false, fmt.Errorf("failed to import session: %w", err)
} else if igsInternal.ID() != session.SessionID {
return false, ErrMismatchingExportedSessionID
}
igs := &InboundGroupSession{
Internal: *igsInternal,
SigningKey: session.SenderClaimedKeys.Ed25519,
SenderKey: session.SenderKey,
RoomID: session.RoomID,
// TODO should we add something here to mark the signing key as unverified like key requests do?
ForwardingChains: session.ForwardingChains,
ReceivedAt: time.Now().UTC(),
}
existingIGS, _ := mach.CryptoStore.GetGroupSession(ctx, igs.RoomID, igs.SenderKey, igs.ID())
if existingIGS != nil && existingIGS.Internal.FirstKnownIndex() <= igs.Internal.FirstKnownIndex() {
// We already have an equivalent or better session in the store, so don't override it.
return false, nil
}
err = mach.CryptoStore.PutGroupSession(ctx, igs.RoomID, igs.SenderKey, igs.ID(), igs)
if err != nil {
return false, fmt.Errorf("failed to store imported session: %w", err)
}
mach.markSessionReceived(ctx, igs.ID())
return true, nil
}
// ImportKeys imports data that was exported with the format specified in the Matrix spec.
// See https://spec.matrix.org/v1.2/client-server-api/#key-exports
func (mach *OlmMachine) ImportKeys(ctx context.Context, passphrase string, data []byte) (int, int, error) {
exportData, err := decodeKeyExport(data)
if err != nil {
return 0, 0, err
}
sessions, err := decryptKeyExport(passphrase, exportData)
if err != nil {
return 0, 0, err
}
count := 0
for _, session := range sessions {
log := mach.Log.With().
Str("room_id", session.RoomID.String()).
Str("session_id", session.SessionID.String()).
Logger()
imported, err := mach.importExportedRoomKey(ctx, session)
if err != nil {
if ctx.Err() != nil {
return count, len(sessions), ctx.Err()
}
log.Error().Err(err).Msg("Failed to import Megolm session from file")
} else if imported {
log.Debug().Msg("Imported Megolm session from file")
count++
} else {
log.Debug().Msg("Skipped Megolm session which is already in the store")
}
}
return count, len(sessions), nil
}