/
key.go
124 lines (106 loc) · 4.12 KB
/
key.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
// 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 ssss
import (
"crypto/rand"
"encoding/base64"
"fmt"
"strings"
"maunium.net/go/mautrix/crypto/utils"
)
// Key represents a SSSS private key and related metadata.
type Key struct {
ID string `json:"-"`
Key []byte `json:"-"`
Metadata *KeyMetadata `json:"-"`
}
// NewKey generates a new SSSS key, optionally based on the given passphrase.
//
// Errors are only returned if crypto/rand runs out of randomness.
func NewKey(passphrase string) (*Key, error) {
// We don't support any other algorithms currently.
keyData := KeyMetadata{Algorithm: AlgorithmAESHMACSHA2}
var ssssKey []byte
if len(passphrase) > 0 {
// There's a passphrase. We need to generate a salt for it, set the metadata
// and then compute the key using the passphrase and the metadata.
saltBytes := make([]byte, 24)
if _, err := rand.Read(saltBytes); err != nil {
return nil, fmt.Errorf("failed to get random bytes for salt: %w", err)
}
keyData.Passphrase = &PassphraseMetadata{
Algorithm: PassphraseAlgorithmPBKDF2,
Iterations: 500000,
Salt: base64.StdEncoding.EncodeToString(saltBytes),
Bits: 256,
}
var err error
ssssKey, err = keyData.Passphrase.GetKey(passphrase)
if err != nil {
return nil, fmt.Errorf("failed to get key from passphrase: %w", err)
}
} else {
// No passphrase, just generate a random key
ssssKey = make([]byte, 32)
if _, err := rand.Read(ssssKey); err != nil {
return nil, fmt.Errorf("failed to get random bytes for key: %w", err)
}
}
// Generate a random ID for the key. It's what identifies the key in account data.
keyIDBytes := make([]byte, 24)
if _, err := rand.Read(keyIDBytes); err != nil {
return nil, fmt.Errorf("failed to get random bytes for key ID: %w", err)
}
// We store a certain hash in the key metadata so that clients can check if the user entered the correct key.
var ivBytes [utils.AESCTRIVLength]byte
if _, err := rand.Read(ivBytes[:]); err != nil {
return nil, fmt.Errorf("failed to get random bytes for IV: %w", err)
}
keyData.IV = base64.StdEncoding.EncodeToString(ivBytes[:])
keyData.MAC = keyData.calculateHash(ssssKey)
return &Key{
Key: ssssKey,
ID: base64.StdEncoding.EncodeToString(keyIDBytes),
Metadata: &keyData,
}, nil
}
// RecoveryKey gets the recovery key for this SSSS key.
func (key *Key) RecoveryKey() string {
return utils.EncodeBase58RecoveryKey(key.Key)
}
// Encrypt encrypts the given data with this key.
func (key *Key) Encrypt(eventType string, data []byte) EncryptedKeyData {
aesKey, hmacKey := utils.DeriveKeysSHA256(key.Key, eventType)
iv := utils.GenA256CTRIV()
payload := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
base64.StdEncoding.Encode(payload, data)
ciphertext := utils.XorA256CTR(payload, aesKey, iv)
return EncryptedKeyData{
Ciphertext: base64.StdEncoding.EncodeToString(ciphertext),
IV: base64.StdEncoding.EncodeToString(iv[:]),
MAC: utils.HMACSHA256B64(ciphertext, hmacKey),
}
}
// Decrypt decrypts the given encrypted data with this key.
func (key *Key) Decrypt(eventType string, data EncryptedKeyData) ([]byte, error) {
var ivBytes [utils.AESCTRIVLength]byte
decodedIV, _ := base64.StdEncoding.DecodeString(data.IV)
copy(ivBytes[:], decodedIV)
ciphertextBytes, err := base64.StdEncoding.DecodeString(data.Ciphertext)
if err != nil {
return nil, err
}
// derive the AES and HMAC keys for the requested event type using the SSSS key
aesKey, hmacKey := utils.DeriveKeysSHA256(key.Key, eventType)
// compare the stored MAC with the one we calculated from the ciphertext
calcMac := utils.HMACSHA256B64(ciphertextBytes, hmacKey)
if strings.ReplaceAll(data.MAC, "=", "") != strings.ReplaceAll(calcMac, "=", "") {
return nil, ErrKeyDataMACMismatch
}
decrypted := utils.XorA256CTR(ciphertextBytes, aesKey, ivBytes)
decryptedDecoded, err := base64.StdEncoding.DecodeString(string(decrypted))
return decryptedDecoded, err
}