Generate and upload SSSS keys with random key or passphrase and store SSSS keys in Olm machine

Signed-off-by: Nikos Filippakis <me@nfil.dev>
This commit is contained in:
Nikos Filippakis 2020-08-21 21:00:10 +02:00
commit 6159b22f94
7 changed files with 226 additions and 83 deletions

View file

@ -604,27 +604,6 @@ func (cli *Client) SetAccountData(name string, data interface{}) (err error) {
return nil
}
// GetAccountData gets the user's account data of this type. See https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-user-userid-account-data-type
func (cli *Client) GetAccountData(name string) (data map[string]interface{}, err error) {
urlPath := cli.BuildURL("user", cli.UserID, "account_data", name)
s := make(map[string]interface{})
_, err = cli.MakeRequest("GET", urlPath, nil, &s)
return s, err
}
// SetAccountData sets the user's account data of this type. See https://matrix.org/docs/spec/client_server/r0.6.0#put-matrix-client-r0-user-userid-account-data-type
func (cli *Client) SetAccountData(name string, data map[string]interface{}) (err error) {
urlPath := cli.BuildURL("user", cli.UserID, "account_data", name)
_, err = cli.MakeRequest("PUT", urlPath, &data, nil)
if err != nil {
return err
}
return nil
}
type ReqSendEvent struct {
Timestamp int64
TransactionID string

View file

@ -55,7 +55,7 @@ type EncryptedFile struct {
}
func NewEncryptedFile() *EncryptedFile {
key, iv := utils.GenA256CTR()
key, iv := utils.GenAttachmentA256CTR()
return &EncryptedFile{
Key: JSONWebKey{
Key: base64.RawURLEncoding.EncodeToString(key[:]),

View file

@ -49,6 +49,8 @@ type OlmMachine struct {
roomKeyRequestFilled *sync.Map
keyVerificationTransactionState *sync.Map
crossSigningKeys *CrossSigningKeysCache
}
// StateStore is used by OlmMachine to get room state information that's needed for encryption.

View file

@ -13,7 +13,8 @@ import (
// PkSigning stores a key pair for signing messages.
type PkSigning struct {
int *C.OlmPkSigning
PublicKey []byte
PublicKey string
Seed []byte
}
func pkSigningSize() uint {
@ -44,24 +45,31 @@ func (p *PkSigning) Clear() {
C.olm_clear_pk_signing((*C.OlmPkSigning)(p.int))
}
// NewPkSigning creates a new PkSigning object, containing a key pair for signing messages.
func NewPkSigning() (*PkSigning, error) {
// NewPkSigningFromSeed creates a new PkSigning object using the given seed.
func NewPkSigningFromSeed(seed []byte) (*PkSigning, error) {
p := newBlackPkSigning()
p.Clear()
pubKey := make([]byte, pkSigningPublicKeyLength())
// Make the slice be at least length 1
random := make([]byte, pkSigningSeedLength())
_, err := rand.Read(random)
if C.olm_pk_signing_key_from_seed((*C.OlmPkSigning)(p.int),
unsafe.Pointer(&pubKey[0]), C.size_t(len(pubKey)),
unsafe.Pointer(&seed[0]), C.size_t(len(seed))) == errorVal() {
return nil, p.lastError()
}
p.PublicKey = string(pubKey)
p.Seed = seed
return p, nil
}
// NewPkSigning creates a new PkSigning object, containing a key pair for signing messages.
func NewPkSigning() (*PkSigning, error) {
// Generate the seed
seed := make([]byte, pkSigningSeedLength())
_, err := rand.Read(seed)
if err != nil {
panic(NotEnoughGoRandom)
}
if C.olm_pk_signing_key_from_seed((*C.OlmPkSigning)(p.int),
unsafe.Pointer(&pubKey[0]), C.size_t(len(pubKey)),
unsafe.Pointer(&random[0]), C.size_t(len(random))) == errorVal() {
return nil, p.lastError()
}
p.PublicKey = pubKey
return p, nil
pk, err := NewPkSigningFromSeed(seed)
return pk, err
}
// Sign creates a signature for the given message using this key.

View file

@ -7,11 +7,12 @@
package crypto
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"strings"
"github.com/pkg/errors"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/crypto/utils"
)
@ -33,12 +34,19 @@ var AccountDataSelfSigningKeyType AccountDataKeyType = "m.cross_signing.self_sig
// SSSSAlgorithm is the type for algorithms used in SSSS.
type SSSSAlgorithm string
// SSSSAlgorithmPBKDF2 is the algorithm used for deriving a key from a SSSS passphrase.
// SSSSAlgorithmPBKDF2 is the algorithm used for deriving a key from an SSSS passphrase.
var SSSSAlgorithmPBKDF2 SSSSAlgorithm = "m.pbkdf2"
// SSSSAlgorithmAESHMACSHA2 is the algorithm used for encrypting and verifying secrets stored on SSSS.
var SSSSAlgorithmAESHMACSHA2 SSSSAlgorithm = "m.secret_storage.v1.aes-hmac-sha2"
// CrossSigningKeysCache holds the three cross-signing keys for the current user.
type CrossSigningKeysCache struct {
MasterKey *olm.PkSigning
SelfSigningKey *olm.PkSigning
UserSigningKey *olm.PkSigning
}
type ssssPassphraseData struct {
Algorithm SSSSAlgorithm `json:"algorithm"`
Iterations int `json:"iterations"`
@ -63,8 +71,10 @@ type ssssEncryptedData struct {
Encrypted map[string]ssssEncryptedKeyData `json:"encrypted"`
}
// getDefaultKeyID retrieves the default key ID for this account from SSSS.
func (mach *OlmMachine) getDefaultKeyID() (string, error) {
data, err := mach.Client.GetAccountData(string(AccountDataDefaultKeyType))
var data map[string]string
err := mach.Client.GetAccountData(string(AccountDataDefaultKeyType), &data)
if err != nil {
return "", err
}
@ -72,24 +82,19 @@ func (mach *OlmMachine) getDefaultKeyID() (string, error) {
if !ok {
return "", errors.New("Could not get default key ID")
}
return keyID.(string), nil
return keyID, nil
}
func (mach *OlmMachine) retrieveDecryptSSSSKey(keyName AccountDataKeyType, keyID string, ssssKey []byte) ([utils.AESCTRKeyLength]byte, error) {
// retrieveDecryptXSigningKey retrieves the requested cross-signing key from SSSS and decrypts it using the given SSSS key.
func (mach *OlmMachine) retrieveDecryptXSigningKey(keyName AccountDataKeyType, keyID string, ssssKey []byte) ([utils.AESCTRKeyLength]byte, error) {
var decryptedKey [utils.AESCTRKeyLength]byte
var encData ssssEncryptedData
data, err := mach.Client.GetAccountData(string(keyName))
if err != nil {
return decryptedKey, err
}
bytes, err := json.Marshal(data)
// retrieve and parse the account data for this key type from SSSS
err := mach.Client.GetAccountData(string(keyName), &encData)
if err != nil {
return decryptedKey, err
}
if err := json.Unmarshal(bytes, &encData); err != nil {
return decryptedKey, err
}
keyEncData, ok := encData.Encrypted[keyID]
if !ok {
@ -105,19 +110,27 @@ func (mach *OlmMachine) retrieveDecryptSSSSKey(keyName AccountDataKeyType, keyID
return decryptedKey, err
}
// derive the AES and HMAC keys for the requested cross-signing key type using the SSSS key
aesKey, hmacKey := utils.DeriveKeysSHA256(ssssKey, string(keyName))
// compare the stored MAC with the one we calculated from the ciphertext
calcMac := utils.HMACSHA256B64(ciphertextBytes, hmacKey)
if strings.ReplaceAll(keyEncData.MAC, "=", "") != strings.ReplaceAll(calcMac, "=", "") {
return decryptedKey, errors.New("Key data MAC mismatch")
}
// use the derived AES key to decrypt the requested cross-signing key seed
decrypted := utils.XorA256CTR(ciphertextBytes, aesKey, ivBytes)
copy(decryptedKey[:], decrypted)
decryptedDecoded, err := base64.StdEncoding.DecodeString(string(decrypted))
if err != nil {
return decryptedKey, err
}
copy(decryptedKey[:], decryptedDecoded)
return decryptedKey, nil
}
// retrieveSSSSKeyData retrieves the data for the requested key from SSSS.
func (mach *OlmMachine) retrieveSSSSKeyData(keyID string) (*ssssKeyData, error) {
var keyData ssssKeyData
data, err := mach.Client.GetAccountData("m.secret_storage.key." + keyID)
@ -136,6 +149,7 @@ func (mach *OlmMachine) retrieveSSSSKeyData(keyID string) (*ssssKeyData, error)
return &keyData, nil
}
// verifySSSSKey verifies the SSSS key is valid by calculating and comparing its MAC.
func verifySSSSKey(ssssKey []byte, iv, mac string) error {
aesKey, hmacKey := utils.DeriveKeysSHA256(ssssKey, "")
@ -155,6 +169,39 @@ func verifySSSSKey(ssssKey []byte, iv, mac string) error {
return nil
}
// keysCacheFromSSSSKey retrieves all the cross-signing keys from SSSS using the given SSSS key and stores them in the olm machine.
func (mach *OlmMachine) keysCacheFromSSSSKey(keyID string, ssssKey []byte) error {
masterKey, err := mach.retrieveDecryptXSigningKey(AccountDataMasterKeyType, keyID, ssssKey)
if err != nil {
return err
}
selfSignKey, err := mach.retrieveDecryptXSigningKey(AccountDataSelfSigningKeyType, keyID, ssssKey)
if err != nil {
return err
}
userSignKey, err := mach.retrieveDecryptXSigningKey(AccountDataUserSigningKeyType, keyID, ssssKey)
if err != nil {
return err
}
var keysCache CrossSigningKeysCache
if keysCache.MasterKey, err = olm.NewPkSigningFromSeed(masterKey[:]); err != nil {
return err
}
if keysCache.SelfSigningKey, err = olm.NewPkSigningFromSeed(selfSignKey[:]); err != nil {
return err
}
if keysCache.UserSigningKey, err = olm.NewPkSigningFromSeed(userSignKey[:]); err != nil {
return err
}
mach.Log.Trace("Retrieved keys from SSSS: Master `%v` Self-signing `%v` User-signing `%v`",
keysCache.MasterKey.PublicKey, keysCache.SelfSigningKey.PublicKey, keysCache.UserSigningKey.PublicKey)
mach.crossSigningKeys = &keysCache
return nil
}
// RetrieveCrossSigningKeysWithPassphrase retrieves the cross-signing keys from SSSS using the given passphrase to decrypt them.
func (mach *OlmMachine) RetrieveCrossSigningKeysWithPassphrase(passphrase string) error {
keyID, err := mach.getDefaultKeyID()
@ -189,22 +236,7 @@ func (mach *OlmMachine) RetrieveCrossSigningKeysWithPassphrase(passphrase string
mach.Log.Debug("Retrieved and verified SSSS key from passphrase")
masterKey, err := mach.retrieveDecryptSSSSKey(AccountDataMasterKeyType, keyID, ssssKey)
if err != nil {
return err
}
selfSignKey, err := mach.retrieveDecryptSSSSKey(AccountDataSelfSigningKeyType, keyID, ssssKey)
if err != nil {
return err
}
userSignKey, err := mach.retrieveDecryptSSSSKey(AccountDataUserSigningKeyType, keyID, ssssKey)
if err != nil {
return err
}
mach.Log.Error("keys %v %v %v", masterKey, selfSignKey, userSignKey)
return nil
return mach.keysCacheFromSSSSKey(keyID, ssssKey)
}
// RetrieveCrossSigningKeysWithRecoveryKey retrieves the cross-signing keys from SSSS using the given recovery key to decrypt them.
@ -221,27 +253,141 @@ func (mach *OlmMachine) RetrieveCrossSigningKeysWithRecoveryKey(recoveryKey stri
}
ssssKey := utils.DecodeBase58RecoveryKey(recoveryKey)
if ssssKey == nil {
return errors.New("Error decoding recovery key")
}
if err := verifySSSSKey(ssssKey[:], keyData.IV, keyData.MAC); err != nil {
if err := verifySSSSKey(ssssKey, keyData.IV, keyData.MAC); err != nil {
return err
}
mach.Log.Debug("Retrieved and verified SSSS key from recovery key")
masterKey, err := mach.retrieveDecryptSSSSKey(AccountDataMasterKeyType, keyID, ssssKey[:])
if err != nil {
return err
}
selfSignKey, err := mach.retrieveDecryptSSSSKey(AccountDataSelfSigningKeyType, keyID, ssssKey[:])
if err != nil {
return err
}
userSignKey, err := mach.retrieveDecryptSSSSKey(AccountDataUserSigningKeyType, keyID, ssssKey[:])
if err != nil {
return err
return mach.keysCacheFromSSSSKey(keyID, ssssKey)
}
// GenerateAndUploadCrossSigningKeys generates a new key with all corresponding cross-signing keys.
// A passphrase can optionally be given for generating the SSSS key, otherwise a random key is used.
// The recovery key for retrieving the SSSS key is returned.
func (mach *OlmMachine) GenerateAndUploadCrossSigningKeys(passphrase ...string) (string, error) {
var ssssKey []byte
newKeyData := ssssKeyData{Algorithm: SSSSAlgorithmAESHMACSHA2}
if len(passphrase) > 0 {
// if a passphrase is given use it to generate an SSSS key and save the parameters used for PBKDF2
var saltBytes [24]byte
if _, err := rand.Read(saltBytes[:]); err != nil {
panic(err)
}
passData := ssssPassphraseData{
Algorithm: SSSSAlgorithmPBKDF2,
Iterations: 500000,
Bits: 256,
Salt: base64.StdEncoding.EncodeToString(saltBytes[:]),
}
newKeyData.Passphrase = &passData
mach.Log.Debug("Generating SSSS key from passphrase")
ssssKey = utils.PBKDF2SHA512([]byte(passphrase[0]), []byte(passData.Salt), passData.Iterations, passData.Bits)
} else {
// if no passphrase generate a random SSSS key
mach.Log.Debug("Generating random SSSS key")
ssssKey = make([]byte, 32)
if _, err := rand.Read(ssssKey); err != nil {
panic(err)
}
}
mach.Log.Error("keys %v %v %v", masterKey, selfSignKey, userSignKey)
var ivBytes [utils.AESCTRIVLength]byte
if _, err := rand.Read(ivBytes[:]); err != nil {
panic(err)
}
// derive the AES and HMAC key for generating the SSSS key's MAC to be uploaded
aesKey, hmacKey := utils.DeriveKeysSHA256(ssssKey, "")
var zeroBytes [utils.AESCTRKeyLength]byte
cipher := utils.XorA256CTR(zeroBytes[:], aesKey, ivBytes)
newKeyData.MAC = utils.HMACSHA256B64(cipher, hmacKey)
newKeyData.IV = base64.StdEncoding.EncodeToString(ivBytes[:])
mach.Log.Debug("Calculated MAC for AES key: `%v`", newKeyData.MAC)
// generate the three cross-signing keys
var keysCache CrossSigningKeysCache
var err error
if keysCache.MasterKey, err = olm.NewPkSigning(); err != nil {
return "", err
}
if keysCache.SelfSigningKey, err = olm.NewPkSigning(); err != nil {
return "", err
}
if keysCache.UserSigningKey, err = olm.NewPkSigning(); err != nil {
return "", err
}
mach.Log.Debug("Generated keys: Master: `%v` Self-signing: `%v` User-signing: `%v`",
keysCache.MasterKey.PublicKey, keysCache.SelfSigningKey.PublicKey, keysCache.UserSigningKey.PublicKey)
// generate a key ID for this SSSS key and store the SSSS key info
var genKeyIDBytes [24]byte
if _, err := rand.Read(genKeyIDBytes[:]); err != nil {
panic(err)
}
genKeyID := base64.StdEncoding.EncodeToString(genKeyIDBytes[:])
mach.Log.Debug("Generated SSSS key ID: `%v`", genKeyID)
if err := mach.Client.SetAccountData("m.secret_storage.key."+genKeyID, newKeyData); err != nil {
return "", err
}
// upload the three cross-signing keys
if err := mach.uploadCrossSigningKeys(genKeyID, ssssKey, &keysCache, true); err != nil {
return "", err
}
// save cross-signing keys, generate and return recovery key
mach.crossSigningKeys = &keysCache
return utils.EncodeBase58RecoveryKey(ssssKey), nil
}
// uploadCrossSigningKeys stores the given cross-signing keys to SSSS under the given key ID,
// optionally setting the key as the default one.
func (mach *OlmMachine) uploadCrossSigningKeys(keyID string, ssssKey []byte, keys *CrossSigningKeysCache, setDefaultKey bool) error {
if setDefaultKey {
mach.Client.SetAccountData(string(AccountDataDefaultKeyType), map[string]interface{}{"key": keyID})
}
if err := mach.uploadCrossSigningSingleKey(keyID, AccountDataMasterKeyType, keys.MasterKey, ssssKey); err != nil {
return err
}
if err := mach.uploadCrossSigningSingleKey(keyID, AccountDataSelfSigningKeyType, keys.SelfSigningKey, ssssKey); err != nil {
return err
}
if err := mach.uploadCrossSigningSingleKey(keyID, AccountDataUserSigningKeyType, keys.UserSigningKey, ssssKey); err != nil {
return err
}
return nil
}
// uploadCrossSigningSingleKey encrypts and uploads a single cross-signing key to SSSS using the given key.
func (mach *OlmMachine) uploadCrossSigningSingleKey(keyID string, keyName AccountDataKeyType, pk *olm.PkSigning, ssssKey []byte) error {
aesKey, hmacKey := utils.DeriveKeysSHA256(ssssKey, string(keyName))
iv := utils.GenA256CTRIV()
plaintextEncoded := base64.StdEncoding.EncodeToString(pk.Seed)
encrypted := utils.XorA256CTR([]byte(plaintextEncoded), aesKey, iv)
macStr := utils.HMACSHA256B64(encrypted, hmacKey)
ivStr := base64.StdEncoding.EncodeToString(iv[:])
mach.Log.Debug("Calculated MAC for key %v with ID `%v`: `%v`", keyName, keyID, macStr)
encryptedXKeyData := ssssEncryptedData{
Encrypted: map[string]ssssEncryptedKeyData{
keyID: {
Ciphertext: base64.StdEncoding.EncodeToString(encrypted),
IV: ivStr,
MAC: macStr,
},
},
}
return mach.Client.SetAccountData(string(keyName), encryptedXKeyData)
}

View file

@ -40,8 +40,8 @@ func XorA256CTR(source []byte, key [AESCTRKeyLength]byte, iv [AESCTRIVLength]byt
return result
}
// GenA256CTR generates a new random AES256-CTR key and IV.
func GenA256CTR() (key [AESCTRKeyLength]byte, iv [AESCTRIVLength]byte) {
// GenAttachmentA256CTR generates a new random AES256-CTR key and IV suitable for encrypting attachments.
func GenAttachmentA256CTR() (key [AESCTRKeyLength]byte, iv [AESCTRIVLength]byte) {
_, err := rand.Read(key[:])
if err != nil {
panic(err)
@ -55,11 +55,21 @@ func GenA256CTR() (key [AESCTRKeyLength]byte, iv [AESCTRIVLength]byte) {
return
}
// GenA256CTRIV generates a random IV for AES256-CTR with the last bit set to zero.
func GenA256CTRIV() (iv [AESCTRIVLength]byte) {
_, err := rand.Read(iv[:])
if err != nil {
panic(err)
}
iv[8] &= 0x7F
return
}
// DeriveKeysSHA256 derives an AES and a HMAC key from the given recovery key.
func DeriveKeysSHA256(key []byte, name string) ([AESCTRKeyLength]byte, [HMACKeyLength]byte) {
var zeroBytes [32]byte
derivedHkdf := hkdf.New(sha256.New, key, zeroBytes[:], []byte(name))
derivedHkdf := hkdf.New(sha256.New, key[:], zeroBytes[:], []byte(name))
var aesKey [AESCTRKeyLength]byte
var hmacKey [HMACKeyLength]byte
@ -75,26 +85,24 @@ func PBKDF2SHA512(password []byte, salt []byte, iters int, keyLenBits int) []byt
}
// DecodeBase58RecoveryKey recovers the secret storage from a recovery key.
func DecodeBase58RecoveryKey(recoveryKey string) [AESCTRKeyLength]byte {
var res [AESCTRKeyLength]byte
func DecodeBase58RecoveryKey(recoveryKey string) []byte {
noSpaces := strings.ReplaceAll(recoveryKey, " ", "")
decoded := base58.Decode(noSpaces)
if len(decoded) != AESCTRKeyLength+3 { // AESCTRKeyLength bytes key and 3 bytes prefix / parity
return res
return nil
}
var parity byte
for _, b := range decoded[:34] {
parity ^= b
}
if parity != decoded[34] || decoded[0] != 0x8B || decoded[1] != 1 {
return res
return nil
}
copy(res[:], decoded[2:34])
return res
return decoded[2:34]
}
// EncodeBase58RecoveryKey recovers the secret storage from a recovery key.
func EncodeBase58RecoveryKey(key [AESCTRKeyLength]byte) string {
func EncodeBase58RecoveryKey(key []byte) string {
var inputBytes [35]byte
copy(inputBytes[2:34], key[:])
inputBytes[0] = 0x8B

View file

@ -13,7 +13,7 @@ import (
func TestAES256Ctr(t *testing.T) {
expected := "Hello world"
key, iv := GenA256CTR()
key, iv := GenAttachmentA256CTR()
enc := XorA256CTR([]byte(expected), key, iv)
dec := XorA256CTR(enc, key, iv)
if string(dec) != expected {