mirror of
https://mau.dev/mautrix/go.git
synced 2026-03-14 14:25:53 +01:00
crypto/ssss: handle slightly broken key metadata better
This commit is contained in:
parent
c4ce008c8e
commit
2c0d51ee7d
3 changed files with 62 additions and 23 deletions
|
|
@ -59,12 +59,12 @@ func NewKey(passphrase string) (*Key, error) {
|
|||
// We store a certain hash in the key metadata so that clients can check if the user entered the correct key.
|
||||
ivBytes := random.Bytes(utils.AESCTRIVLength)
|
||||
keyData.IV = base64.RawStdEncoding.EncodeToString(ivBytes)
|
||||
var err error
|
||||
keyData.MAC, err = keyData.calculateHash(ssssKey)
|
||||
macBytes, err := keyData.calculateHash(ssssKey)
|
||||
if err != nil {
|
||||
// This should never happen because we just generated the IV and key.
|
||||
return nil, fmt.Errorf("failed to calculate hash: %w", err)
|
||||
}
|
||||
keyData.MAC = base64.RawStdEncoding.EncodeToString(macBytes)
|
||||
|
||||
return &Key{
|
||||
Key: ssssKey,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
package ssss
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -74,11 +76,16 @@ func (kd *KeyMetadata) verifyKey(key []byte) error {
|
|||
if len(unpaddedMAC) != expectedMACLength {
|
||||
return fmt.Errorf("%w: invalid mac length %d (expected %d)", ErrCorruptedKeyMetadata, len(unpaddedMAC), expectedMACLength)
|
||||
}
|
||||
hash, err := kd.calculateHash(key)
|
||||
expectedMAC, err := base64.RawStdEncoding.DecodeString(unpaddedMAC)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: failed to decode mac: %w", ErrCorruptedKeyMetadata, err)
|
||||
}
|
||||
calculatedMAC, err := kd.calculateHash(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if unpaddedMAC != hash {
|
||||
// This doesn't really need to be constant time since it's fully local, but might as well be.
|
||||
if !hmac.Equal(expectedMAC, calculatedMAC) {
|
||||
return ErrIncorrectSSSSKey
|
||||
}
|
||||
return nil
|
||||
|
|
@ -91,23 +98,26 @@ func (kd *KeyMetadata) VerifyKey(key []byte) bool {
|
|||
|
||||
// calculateHash calculates the hash used for checking if the key is entered correctly as described
|
||||
// in the spec: https://matrix.org/docs/spec/client_server/unstable#m-secret-storage-v1-aes-hmac-sha2
|
||||
func (kd *KeyMetadata) calculateHash(key []byte) (string, error) {
|
||||
func (kd *KeyMetadata) calculateHash(key []byte) ([]byte, error) {
|
||||
aesKey, hmacKey := utils.DeriveKeysSHA256(key, "")
|
||||
unpaddedIV := strings.TrimRight(kd.IV, "=")
|
||||
expectedIVLength := base64.RawStdEncoding.EncodedLen(utils.AESCTRIVLength)
|
||||
if len(unpaddedIV) != expectedIVLength {
|
||||
return "", fmt.Errorf("%w: invalid iv length %d (expected %d)", ErrCorruptedKeyMetadata, len(unpaddedIV), expectedIVLength)
|
||||
if len(unpaddedIV) < expectedIVLength || len(unpaddedIV) > expectedIVLength*3 {
|
||||
return nil, fmt.Errorf("%w: invalid iv length %d (expected %d)", ErrCorruptedKeyMetadata, len(unpaddedIV), expectedIVLength)
|
||||
}
|
||||
|
||||
var ivBytes [utils.AESCTRIVLength]byte
|
||||
_, err := base64.RawStdEncoding.Decode(ivBytes[:], []byte(unpaddedIV))
|
||||
rawIVBytes, err := base64.RawStdEncoding.DecodeString(unpaddedIV)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: failed to decode iv: %w", ErrCorruptedKeyMetadata, err)
|
||||
return nil, fmt.Errorf("%w: failed to decode iv: %w", ErrCorruptedKeyMetadata, err)
|
||||
}
|
||||
// TODO log a warning for non-16 byte IVs?
|
||||
// Certain broken clients like nheko generated 32-byte IVs where only the first 16 bytes were used.
|
||||
ivBytes := *(*[utils.AESCTRIVLength]byte)(rawIVBytes[:utils.AESCTRIVLength])
|
||||
|
||||
cipher := utils.XorA256CTR(make([]byte, utils.AESCTRKeyLength), aesKey, ivBytes)
|
||||
|
||||
return utils.HMACSHA256B64(cipher, hmacKey), nil
|
||||
zeroes := make([]byte, utils.AESCTRKeyLength)
|
||||
encryptedZeroes := utils.XorA256CTR(zeroes, aesKey, ivBytes)
|
||||
h := hmac.New(sha256.New, hmacKey[:])
|
||||
h.Write(encryptedZeroes)
|
||||
return h.Sum(nil), nil
|
||||
}
|
||||
|
||||
// PassphraseMetadata represents server-side metadata about a SSSS key passphrase.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ package ssss_test
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -42,10 +41,24 @@ const key2Meta = `
|
|||
}
|
||||
`
|
||||
|
||||
const key2MetaUnverified = `
|
||||
{
|
||||
"algorithm": "m.secret_storage.v1.aes-hmac-sha2"
|
||||
}
|
||||
`
|
||||
|
||||
const key2MetaLongIV = `
|
||||
{
|
||||
"algorithm": "m.secret_storage.v1.aes-hmac-sha2",
|
||||
"iv": "O0BOvTqiIAYjC+RMcyHfW2f/gdxjceTxoYtNlpPduJ8=",
|
||||
"mac": "7k6OruQlWg0UmQjxGZ0ad4Q6DdwkgnoI7G6X3IjBYtI="
|
||||
}
|
||||
`
|
||||
|
||||
const key2MetaBrokenIV = `
|
||||
{
|
||||
"algorithm": "m.secret_storage.v1.aes-hmac-sha2",
|
||||
"iv": "O0BOvTqiIAYjC+RMcyHfWwMeowMeowMeow",
|
||||
"iv": "MeowMeowMeow",
|
||||
"mac": "7k6OruQlWg0UmQjxGZ0ad4Q6DdwkgnoI7G6X3IjBYtI="
|
||||
}
|
||||
`
|
||||
|
|
@ -94,17 +107,33 @@ func TestKeyMetadata_VerifyRecoveryKey_Correct2(t *testing.T) {
|
|||
assert.Equal(t, key2RecoveryKey, key.RecoveryKey())
|
||||
}
|
||||
|
||||
func TestKeyMetadata_VerifyRecoveryKey_NonCompliant_LongIV(t *testing.T) {
|
||||
km := getKeyMeta(key2MetaLongIV)
|
||||
key, err := km.VerifyRecoveryKey(key2ID, key2RecoveryKey)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, key)
|
||||
assert.Equal(t, key2RecoveryKey, key.RecoveryKey())
|
||||
}
|
||||
|
||||
func TestKeyMetadata_VerifyRecoveryKey_Unverified(t *testing.T) {
|
||||
km := getKeyMeta(key2MetaUnverified)
|
||||
key, err := km.VerifyRecoveryKey(key2ID, key2RecoveryKey)
|
||||
assert.ErrorIs(t, err, ssss.ErrUnverifiableKey)
|
||||
assert.NotNil(t, key)
|
||||
assert.Equal(t, key2RecoveryKey, key.RecoveryKey())
|
||||
}
|
||||
|
||||
func TestKeyMetadata_VerifyRecoveryKey_Invalid(t *testing.T) {
|
||||
km := getKeyMeta(key1Meta)
|
||||
key, err := km.VerifyRecoveryKey(key1ID, "foo")
|
||||
assert.True(t, errors.Is(err, ssss.ErrInvalidRecoveryKey), "unexpected error: %v", err)
|
||||
assert.ErrorIs(t, err, ssss.ErrInvalidRecoveryKey)
|
||||
assert.Nil(t, key)
|
||||
}
|
||||
|
||||
func TestKeyMetadata_VerifyRecoveryKey_Incorrect(t *testing.T) {
|
||||
km := getKeyMeta(key1Meta)
|
||||
key, err := km.VerifyRecoveryKey(key2ID, key2RecoveryKey)
|
||||
assert.True(t, errors.Is(err, ssss.ErrIncorrectSSSSKey), "unexpected error: %v", err)
|
||||
assert.ErrorIs(t, err, ssss.ErrIncorrectSSSSKey)
|
||||
assert.Nil(t, key)
|
||||
}
|
||||
|
||||
|
|
@ -119,27 +148,27 @@ func TestKeyMetadata_VerifyPassphrase_Correct(t *testing.T) {
|
|||
func TestKeyMetadata_VerifyPassphrase_Incorrect(t *testing.T) {
|
||||
km := getKeyMeta(key1Meta)
|
||||
key, err := km.VerifyPassphrase(key1ID, "incorrect horse battery staple")
|
||||
assert.True(t, errors.Is(err, ssss.ErrIncorrectSSSSKey), "unexpected error %v", err)
|
||||
assert.ErrorIs(t, err, ssss.ErrIncorrectSSSSKey)
|
||||
assert.Nil(t, key)
|
||||
}
|
||||
|
||||
func TestKeyMetadata_VerifyPassphrase_NotSet(t *testing.T) {
|
||||
km := getKeyMeta(key2Meta)
|
||||
key, err := km.VerifyPassphrase(key2ID, "hmm")
|
||||
assert.True(t, errors.Is(err, ssss.ErrNoPassphrase), "unexpected error %v", err)
|
||||
assert.ErrorIs(t, err, ssss.ErrNoPassphrase)
|
||||
assert.Nil(t, key)
|
||||
}
|
||||
|
||||
func TestKeyMetadata_VerifyRecoveryKey_CorruptedIV(t *testing.T) {
|
||||
km := getKeyMeta(key2MetaBrokenIV)
|
||||
key, err := km.VerifyRecoveryKey(key2ID, key2RecoveryKey)
|
||||
assert.True(t, errors.Is(err, ssss.ErrCorruptedKeyMetadata), "unexpected error %v", err)
|
||||
assert.ErrorIs(t, err, ssss.ErrCorruptedKeyMetadata)
|
||||
assert.Nil(t, key)
|
||||
}
|
||||
|
||||
func TestKeyMetadata_VerifyRecoveryKey_CorruptedMAC(t *testing.T) {
|
||||
km := getKeyMeta(key2MetaBrokenMAC)
|
||||
key, err := km.VerifyRecoveryKey(key2ID, key2RecoveryKey)
|
||||
assert.True(t, errors.Is(err, ssss.ErrCorruptedKeyMetadata), "unexpected error %v", err)
|
||||
assert.ErrorIs(t, err, ssss.ErrCorruptedKeyMetadata)
|
||||
assert.Nil(t, key)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue