From 6159b22f948616f3bdf34f11d64f1ae335b735bd Mon Sep 17 00:00:00 2001 From: Nikos Filippakis Date: Fri, 21 Aug 2020 21:00:10 +0200 Subject: [PATCH] Generate and upload SSSS keys with random key or passphrase and store SSSS keys in Olm machine Signed-off-by: Nikos Filippakis --- client.go | 21 --- crypto/attachment/attachments.go | 2 +- crypto/machine.go | 2 + crypto/olm/pk.go | 34 +++-- crypto/ssss.go | 232 +++++++++++++++++++++++++------ crypto/utils/utils.go | 28 ++-- crypto/utils/utils_test.go | 2 +- 7 files changed, 232 insertions(+), 89 deletions(-) diff --git a/client.go b/client.go index 34f4ca78..e5d0b815 100644 --- a/client.go +++ b/client.go @@ -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 diff --git a/crypto/attachment/attachments.go b/crypto/attachment/attachments.go index 0524b6f7..fa0e331a 100644 --- a/crypto/attachment/attachments.go +++ b/crypto/attachment/attachments.go @@ -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[:]), diff --git a/crypto/machine.go b/crypto/machine.go index f1537d7b..e69704e3 100644 --- a/crypto/machine.go +++ b/crypto/machine.go @@ -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. diff --git a/crypto/olm/pk.go b/crypto/olm/pk.go index e36279a5..8cbb53c9 100644 --- a/crypto/olm/pk.go +++ b/crypto/olm/pk.go @@ -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. diff --git a/crypto/ssss.go b/crypto/ssss.go index 8778c384..82f4b66e 100644 --- a/crypto/ssss.go +++ b/crypto/ssss.go @@ -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) +} diff --git a/crypto/utils/utils.go b/crypto/utils/utils.go index 60b83e69..2a013170 100644 --- a/crypto/utils/utils.go +++ b/crypto/utils/utils.go @@ -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 diff --git a/crypto/utils/utils_test.go b/crypto/utils/utils_test.go index 446d4877..eb700b2a 100644 --- a/crypto/utils/utils_test.go +++ b/crypto/utils/utils_test.go @@ -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 {