diff --git a/crypto/goolm/account/account.go b/crypto/goolm/account/account.go index 099cc493..d031ec53 100644 --- a/crypto/goolm/account/account.go +++ b/crypto/goolm/account/account.go @@ -8,7 +8,6 @@ import ( "maunium.net/go/mautrix/id" - "maunium.net/go/mautrix/crypto/goolm/cipher" "maunium.net/go/mautrix/crypto/goolm/crypto" "maunium.net/go/mautrix/crypto/goolm/libolmpickle" "maunium.net/go/mautrix/crypto/goolm/session" @@ -322,7 +321,7 @@ func (a *Account) ForgetOldFallbackKey() { // Unpickle decodes the base64 encoded string and decrypts the result with the key. // The decrypted value is then passed to UnpickleLibOlm. func (a *Account) Unpickle(pickled, key []byte) error { - decrypted, err := cipher.Unpickle(key, pickled) + decrypted, err := libolmpickle.Unpickle(key, pickled) if err != nil { return err } @@ -410,7 +409,7 @@ func (a *Account) Pickle(key []byte) ([]byte, error) { if len(key) == 0 { return nil, olm.ErrNoKeyProvided } - return cipher.Pickle(key, a.PickleLibOlm()) + return libolmpickle.Pickle(key, a.PickleLibOlm()) } // PickleLibOlm pickles the [Account] and returns the raw bytes. diff --git a/crypto/goolm/cipher/pickle.go b/crypto/goolm/cipher/pickle.go deleted file mode 100644 index d37c7112..00000000 --- a/crypto/goolm/cipher/pickle.go +++ /dev/null @@ -1,51 +0,0 @@ -package cipher - -import ( - "crypto/aes" - "fmt" - - "maunium.net/go/mautrix/crypto/aessha2" - "maunium.net/go/mautrix/crypto/goolm/goolmbase64" - "maunium.net/go/mautrix/crypto/olm" -) - -const pickleMACLength = 8 - -var kdfPickle = []byte("Pickle") //used to derive the keys for encryption - -// PickleBlockSize is the blocksize of the pickle cipher. -const PickleBlockSize = aes.BlockSize - -// Pickle encrypts the input with the key and the cipher AESSHA256. The result is then encoded in base64. -func Pickle(key, plaintext []byte) ([]byte, error) { - if c, err := aessha2.NewAESSHA2(key, kdfPickle); err != nil { - return nil, err - } else if ciphertext, err := c.Encrypt(plaintext); err != nil { - return nil, err - } else if mac, err := c.MAC(ciphertext); err != nil { - return nil, err - } else { - return goolmbase64.Encode(append(ciphertext, mac[:pickleMACLength]...)), nil - } -} - -// Unpickle decodes the input from base64 and decrypts the decoded input with the key and the cipher AESSHA256. -func Unpickle(key, input []byte) ([]byte, error) { - ciphertext, err := goolmbase64.Decode(input) - if err != nil { - return nil, err - } - ciphertext, mac := ciphertext[:len(ciphertext)-pickleMACLength], ciphertext[len(ciphertext)-pickleMACLength:] - if c, err := aessha2.NewAESSHA2(key, kdfPickle); err != nil { - return nil, err - } else if verified, err := c.VerifyMAC(ciphertext, mac); err != nil { - return nil, err - } else if !verified { - return nil, fmt.Errorf("decrypt pickle: %w", olm.ErrBadMAC) - } else { - // Set to next block size - targetCipherText := make([]byte, int(len(ciphertext)/PickleBlockSize)*PickleBlockSize) - copy(targetCipherText, ciphertext) - return c.Decrypt(targetCipherText) - } -} diff --git a/crypto/goolm/cipher/pickle_test.go b/crypto/goolm/cipher/pickle_test.go deleted file mode 100644 index b6cfe809..00000000 --- a/crypto/goolm/cipher/pickle_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package cipher_test - -import ( - "crypto/aes" - "testing" - - "github.com/stretchr/testify/assert" - - "maunium.net/go/mautrix/crypto/goolm/cipher" -) - -func TestEncoding(t *testing.T) { - key := []byte("test key") - input := []byte("test") - //pad marshaled to get block size - toEncrypt := input - if len(input)%aes.BlockSize != 0 { - padding := aes.BlockSize - len(input)%aes.BlockSize - toEncrypt = make([]byte, len(input)+padding) - copy(toEncrypt, input) - } - encoded, err := cipher.Pickle(key, toEncrypt) - assert.NoError(t, err) - - decoded, err := cipher.Unpickle(key, encoded) - assert.NoError(t, err) - assert.Equal(t, toEncrypt, decoded) -} diff --git a/crypto/goolm/libolmpickle/encoder.go b/crypto/goolm/libolmpickle/encoder.go new file mode 100644 index 00000000..63e7b09b --- /dev/null +++ b/crypto/goolm/libolmpickle/encoder.go @@ -0,0 +1,40 @@ +package libolmpickle + +import ( + "bytes" + "encoding/binary" + + "go.mau.fi/util/exerrors" +) + +const ( + PickleBoolLength = 1 + PickleUInt8Length = 1 + PickleUInt32Length = 4 +) + +type Encoder struct { + bytes.Buffer +} + +func NewEncoder() *Encoder { return &Encoder{} } + +func (p *Encoder) WriteUInt8(value uint8) { + exerrors.PanicIfNotNil(p.WriteByte(value)) +} + +func (p *Encoder) WriteBool(value bool) { + if value { + exerrors.PanicIfNotNil(p.WriteByte(0x01)) + } else { + exerrors.PanicIfNotNil(p.WriteByte(0x00)) + } +} + +func (p *Encoder) WriteEmptyBytes(count int) { + exerrors.Must(p.Write(make([]byte, count))) +} + +func (p *Encoder) WriteUInt32(value uint32) { + exerrors.PanicIfNotNil(binary.Write(&p.Buffer, binary.BigEndian, value)) +} diff --git a/crypto/goolm/libolmpickle/encoder_test.go b/crypto/goolm/libolmpickle/encoder_test.go new file mode 100644 index 00000000..c7811225 --- /dev/null +++ b/crypto/goolm/libolmpickle/encoder_test.go @@ -0,0 +1,99 @@ +package libolmpickle_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "maunium.net/go/mautrix/crypto/goolm/libolmpickle" +) + +func TestEncoder(t *testing.T) { + var encoder libolmpickle.Encoder + encoder.WriteUInt32(4) + encoder.WriteUInt8(8) + encoder.WriteBool(false) + encoder.WriteEmptyBytes(10) + encoder.WriteBool(true) + encoder.Write([]byte("test")) + encoder.WriteUInt32(420_000) + assert.Equal(t, []byte{ + 0x00, 0x00, 0x00, 0x04, // 4 + 0x08, // 8 + 0x00, // false + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ten empty bytes + 0x01, //true + 0x74, 0x65, 0x73, 0x74, // "test" (ASCII) + 0x00, 0x06, 0x68, 0xa0, // 420,000 + }, encoder.Bytes()) +} + +func TestPickleUInt32(t *testing.T) { + values := []uint32{ + 0xffffffff, + 0x00ff00ff, + 0xf0000000, + 0xf00f0000, + } + expected := [][]byte{ + {0xff, 0xff, 0xff, 0xff}, + {0x00, 0xff, 0x00, 0xff}, + {0xf0, 0x00, 0x00, 0x00}, + {0xf0, 0x0f, 0x00, 0x00}, + } + for i, value := range values { + var encoder libolmpickle.Encoder + encoder.WriteUInt32(value) + assert.Equal(t, expected[i], encoder.Bytes()) + } +} + +func TestPickleBool(t *testing.T) { + values := []bool{ + true, + false, + } + expected := [][]byte{ + {0x01}, + {0x00}, + } + for i, value := range values { + var encoder libolmpickle.Encoder + encoder.WriteBool(value) + assert.Equal(t, expected[i], encoder.Bytes()) + } +} + +func TestPickleUInt8(t *testing.T) { + values := []uint8{ + 0xff, + 0x1a, + } + expected := [][]byte{ + {0xff}, + {0x1a}, + } + for i, value := range values { + var encoder libolmpickle.Encoder + encoder.WriteUInt8(value) + assert.Equal(t, expected[i], encoder.Bytes()) + } +} + +func TestPickleBytes(t *testing.T) { + values := [][]byte{ + {0xff, 0xff, 0xff, 0xff}, + {0x00, 0xff, 0x00, 0xff}, + {0xf0, 0x00, 0x00, 0x00}, + } + expected := [][]byte{ + {0xff, 0xff, 0xff, 0xff}, + {0x00, 0xff, 0x00, 0xff}, + {0xf0, 0x00, 0x00, 0x00}, + } + for i, value := range values { + var encoder libolmpickle.Encoder + encoder.Write(value) + assert.Equal(t, expected[i], encoder.Bytes()) + } +} diff --git a/crypto/goolm/libolmpickle/pickle.go b/crypto/goolm/libolmpickle/pickle.go index 63e7b09b..ab4030bf 100644 --- a/crypto/goolm/libolmpickle/pickle.go +++ b/crypto/goolm/libolmpickle/pickle.go @@ -1,40 +1,51 @@ package libolmpickle import ( - "bytes" - "encoding/binary" + "crypto/aes" + "fmt" - "go.mau.fi/util/exerrors" + "maunium.net/go/mautrix/crypto/aessha2" + "maunium.net/go/mautrix/crypto/goolm/goolmbase64" + "maunium.net/go/mautrix/crypto/olm" ) -const ( - PickleBoolLength = 1 - PickleUInt8Length = 1 - PickleUInt32Length = 4 -) +const pickleMACLength = 8 -type Encoder struct { - bytes.Buffer -} +var kdfPickle = []byte("Pickle") //used to derive the keys for encryption -func NewEncoder() *Encoder { return &Encoder{} } +// PickleBlockSize is the blocksize of the pickle cipher. +const PickleBlockSize = aes.BlockSize -func (p *Encoder) WriteUInt8(value uint8) { - exerrors.PanicIfNotNil(p.WriteByte(value)) -} - -func (p *Encoder) WriteBool(value bool) { - if value { - exerrors.PanicIfNotNil(p.WriteByte(0x01)) +// Pickle encrypts the input with the key and the cipher AESSHA256. The result is then encoded in base64. +func Pickle(key, plaintext []byte) ([]byte, error) { + if c, err := aessha2.NewAESSHA2(key, kdfPickle); err != nil { + return nil, err + } else if ciphertext, err := c.Encrypt(plaintext); err != nil { + return nil, err + } else if mac, err := c.MAC(ciphertext); err != nil { + return nil, err } else { - exerrors.PanicIfNotNil(p.WriteByte(0x00)) + return goolmbase64.Encode(append(ciphertext, mac[:pickleMACLength]...)), nil } } -func (p *Encoder) WriteEmptyBytes(count int) { - exerrors.Must(p.Write(make([]byte, count))) -} - -func (p *Encoder) WriteUInt32(value uint32) { - exerrors.PanicIfNotNil(binary.Write(&p.Buffer, binary.BigEndian, value)) +// Unpickle decodes the input from base64 and decrypts the decoded input with the key and the cipher AESSHA256. +func Unpickle(key, input []byte) ([]byte, error) { + ciphertext, err := goolmbase64.Decode(input) + if err != nil { + return nil, err + } + ciphertext, mac := ciphertext[:len(ciphertext)-pickleMACLength], ciphertext[len(ciphertext)-pickleMACLength:] + if c, err := aessha2.NewAESSHA2(key, kdfPickle); err != nil { + return nil, err + } else if verified, err := c.VerifyMAC(ciphertext, mac); err != nil { + return nil, err + } else if !verified { + return nil, fmt.Errorf("decrypt pickle: %w", olm.ErrBadMAC) + } else { + // Set to next block size + targetCipherText := make([]byte, int(len(ciphertext)/PickleBlockSize)*PickleBlockSize) + copy(targetCipherText, ciphertext) + return c.Decrypt(targetCipherText) + } } diff --git a/crypto/goolm/libolmpickle/pickle_test.go b/crypto/goolm/libolmpickle/pickle_test.go index c7811225..0720e008 100644 --- a/crypto/goolm/libolmpickle/pickle_test.go +++ b/crypto/goolm/libolmpickle/pickle_test.go @@ -1,99 +1,26 @@ -package libolmpickle_test +package libolmpickle import ( + "crypto/aes" "testing" "github.com/stretchr/testify/assert" - - "maunium.net/go/mautrix/crypto/goolm/libolmpickle" ) -func TestEncoder(t *testing.T) { - var encoder libolmpickle.Encoder - encoder.WriteUInt32(4) - encoder.WriteUInt8(8) - encoder.WriteBool(false) - encoder.WriteEmptyBytes(10) - encoder.WriteBool(true) - encoder.Write([]byte("test")) - encoder.WriteUInt32(420_000) - assert.Equal(t, []byte{ - 0x00, 0x00, 0x00, 0x04, // 4 - 0x08, // 8 - 0x00, // false - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ten empty bytes - 0x01, //true - 0x74, 0x65, 0x73, 0x74, // "test" (ASCII) - 0x00, 0x06, 0x68, 0xa0, // 420,000 - }, encoder.Bytes()) -} - -func TestPickleUInt32(t *testing.T) { - values := []uint32{ - 0xffffffff, - 0x00ff00ff, - 0xf0000000, - 0xf00f0000, - } - expected := [][]byte{ - {0xff, 0xff, 0xff, 0xff}, - {0x00, 0xff, 0x00, 0xff}, - {0xf0, 0x00, 0x00, 0x00}, - {0xf0, 0x0f, 0x00, 0x00}, - } - for i, value := range values { - var encoder libolmpickle.Encoder - encoder.WriteUInt32(value) - assert.Equal(t, expected[i], encoder.Bytes()) - } -} - -func TestPickleBool(t *testing.T) { - values := []bool{ - true, - false, - } - expected := [][]byte{ - {0x01}, - {0x00}, - } - for i, value := range values { - var encoder libolmpickle.Encoder - encoder.WriteBool(value) - assert.Equal(t, expected[i], encoder.Bytes()) - } -} - -func TestPickleUInt8(t *testing.T) { - values := []uint8{ - 0xff, - 0x1a, - } - expected := [][]byte{ - {0xff}, - {0x1a}, - } - for i, value := range values { - var encoder libolmpickle.Encoder - encoder.WriteUInt8(value) - assert.Equal(t, expected[i], encoder.Bytes()) - } -} - -func TestPickleBytes(t *testing.T) { - values := [][]byte{ - {0xff, 0xff, 0xff, 0xff}, - {0x00, 0xff, 0x00, 0xff}, - {0xf0, 0x00, 0x00, 0x00}, - } - expected := [][]byte{ - {0xff, 0xff, 0xff, 0xff}, - {0x00, 0xff, 0x00, 0xff}, - {0xf0, 0x00, 0x00, 0x00}, - } - for i, value := range values { - var encoder libolmpickle.Encoder - encoder.Write(value) - assert.Equal(t, expected[i], encoder.Bytes()) - } +func TestEncoding(t *testing.T) { + key := []byte("test key") + input := []byte("test") + //pad marshaled to get block size + toEncrypt := input + if len(input)%aes.BlockSize != 0 { + padding := aes.BlockSize - len(input)%aes.BlockSize + toEncrypt = make([]byte, len(input)+padding) + copy(toEncrypt, input) + } + encoded, err := Pickle(key, toEncrypt) + assert.NoError(t, err) + + decoded, err := Unpickle(key, encoded) + assert.NoError(t, err) + assert.Equal(t, toEncrypt, decoded) } diff --git a/crypto/goolm/pk/decryption.go b/crypto/goolm/pk/decryption.go index f2d0cb66..ac3a05a6 100644 --- a/crypto/goolm/pk/decryption.go +++ b/crypto/goolm/pk/decryption.go @@ -5,7 +5,6 @@ import ( "fmt" "maunium.net/go/mautrix/crypto/aessha2" - "maunium.net/go/mautrix/crypto/goolm/cipher" "maunium.net/go/mautrix/crypto/goolm/crypto" "maunium.net/go/mautrix/crypto/goolm/goolmbase64" "maunium.net/go/mautrix/crypto/goolm/libolmpickle" @@ -88,7 +87,7 @@ func (a *Decryption) UnpickleAsJSON(pickled, key []byte) error { // Unpickle decodes the base64 encoded string and decrypts the result with the key. // The decrypted value is then passed to UnpickleLibOlm. func (a *Decryption) Unpickle(pickled, key []byte) error { - decrypted, err := cipher.Unpickle(key, pickled) + decrypted, err := libolmpickle.Unpickle(key, pickled) if err != nil { return err } @@ -111,7 +110,7 @@ func (a *Decryption) UnpickleLibOlm(unpickled []byte) error { // Pickle returns a base64 encoded and with key encrypted pickled Decryption using PickleLibOlm(). func (a Decryption) Pickle(key []byte) ([]byte, error) { - return cipher.Pickle(key, a.PickleLibOlm()) + return libolmpickle.Pickle(key, a.PickleLibOlm()) } // PickleLibOlm pickles the [Decryption] into the encoder. diff --git a/crypto/goolm/session/megolm_inbound_session.go b/crypto/goolm/session/megolm_inbound_session.go index 4c107e92..4a4ab4d7 100644 --- a/crypto/goolm/session/megolm_inbound_session.go +++ b/crypto/goolm/session/megolm_inbound_session.go @@ -4,7 +4,6 @@ import ( "encoding/base64" "fmt" - "maunium.net/go/mautrix/crypto/goolm/cipher" "maunium.net/go/mautrix/crypto/goolm/crypto" "maunium.net/go/mautrix/crypto/goolm/goolmbase64" "maunium.net/go/mautrix/crypto/goolm/libolmpickle" @@ -192,7 +191,7 @@ func (o *MegolmInboundSession) Unpickle(pickled, key []byte) error { } else if len(pickled) == 0 { return olm.ErrEmptyInput } - decrypted, err := cipher.Unpickle(key, pickled) + decrypted, err := libolmpickle.Unpickle(key, pickled) if err != nil { return err } @@ -234,7 +233,7 @@ func (o *MegolmInboundSession) Pickle(key []byte) ([]byte, error) { if len(key) == 0 { return nil, olm.ErrNoKeyProvided } - return cipher.Pickle(key, o.PickleLibOlm()) + return libolmpickle.Pickle(key, o.PickleLibOlm()) } // PickleLibOlm pickles the session returning the raw bytes. diff --git a/crypto/goolm/session/megolm_outbound_session.go b/crypto/goolm/session/megolm_outbound_session.go index 6164f965..4f4f7820 100644 --- a/crypto/goolm/session/megolm_outbound_session.go +++ b/crypto/goolm/session/megolm_outbound_session.go @@ -7,7 +7,6 @@ import ( "go.mau.fi/util/exerrors" - "maunium.net/go/mautrix/crypto/goolm/cipher" "maunium.net/go/mautrix/crypto/goolm/crypto" "maunium.net/go/mautrix/crypto/goolm/goolmbase64" "maunium.net/go/mautrix/crypto/goolm/libolmpickle" @@ -91,7 +90,7 @@ func (o *MegolmOutboundSession) Unpickle(pickled, key []byte) error { if len(key) == 0 { return olm.ErrNoKeyProvided } - decrypted, err := cipher.Unpickle(key, pickled) + decrypted, err := libolmpickle.Unpickle(key, pickled) if err != nil { return err } @@ -117,7 +116,7 @@ func (o *MegolmOutboundSession) Pickle(key []byte) ([]byte, error) { if len(key) == 0 { return nil, olm.ErrNoKeyProvided } - return cipher.Pickle(key, o.PickleLibOlm()) + return libolmpickle.Pickle(key, o.PickleLibOlm()) } // PickleLibOlm pickles the session returning the raw bytes. diff --git a/crypto/goolm/session/olm_session.go b/crypto/goolm/session/olm_session.go index fcd9d0dc..acded681 100644 --- a/crypto/goolm/session/olm_session.go +++ b/crypto/goolm/session/olm_session.go @@ -7,7 +7,6 @@ import ( "fmt" "strings" - "maunium.net/go/mautrix/crypto/goolm/cipher" "maunium.net/go/mautrix/crypto/goolm/crypto" "maunium.net/go/mautrix/crypto/goolm/goolmbase64" "maunium.net/go/mautrix/crypto/goolm/libolmpickle" @@ -355,7 +354,7 @@ func (o *OlmSession) Unpickle(pickled, key []byte) error { if len(pickled) == 0 { return olm.ErrEmptyInput } - decrypted, err := cipher.Unpickle(key, pickled) + decrypted, err := libolmpickle.Unpickle(key, pickled) if err != nil { return err } @@ -396,7 +395,7 @@ func (s *OlmSession) Pickle(key []byte) ([]byte, error) { if len(key) == 0 { return nil, olm.ErrNoKeyProvided } - return cipher.Pickle(key, s.PickleLibOlm()) + return libolmpickle.Pickle(key, s.PickleLibOlm()) } // PickleLibOlm pickles the session and returns the raw bytes. diff --git a/crypto/goolm/utilities/pickle.go b/crypto/goolm/utilities/pickle.go index c6d9d693..ad678c6b 100644 --- a/crypto/goolm/utilities/pickle.go +++ b/crypto/goolm/utilities/pickle.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "maunium.net/go/mautrix/crypto/goolm/cipher" + "maunium.net/go/mautrix/crypto/goolm/libolmpickle" "maunium.net/go/mautrix/crypto/olm" ) @@ -21,12 +21,12 @@ func PickleAsJSON(object any, pickleVersion byte, key []byte) ([]byte, error) { toEncrypt := make([]byte, len(marshaled)) copy(toEncrypt, marshaled) //pad marshaled to get block size - if len(marshaled)%cipher.PickleBlockSize != 0 { - padding := cipher.PickleBlockSize - len(marshaled)%cipher.PickleBlockSize + if len(marshaled)%libolmpickle.PickleBlockSize != 0 { + padding := libolmpickle.PickleBlockSize - len(marshaled)%libolmpickle.PickleBlockSize toEncrypt = make([]byte, len(marshaled)+padding) copy(toEncrypt, marshaled) } - encrypted, err := cipher.Pickle(key, toEncrypt) + encrypted, err := libolmpickle.Pickle(key, toEncrypt) if err != nil { return nil, fmt.Errorf("pickle encrypt: %w", err) } @@ -38,7 +38,7 @@ func UnpickleAsJSON(object any, pickled, key []byte, pickleVersion byte) error { if len(key) == 0 { return fmt.Errorf("unpickle: %w", olm.ErrNoKeyProvided) } - decrypted, err := cipher.Unpickle(key, pickled) + decrypted, err := libolmpickle.Unpickle(key, pickled) if err != nil { return fmt.Errorf("unpickle decrypt: %w", err) } diff --git a/crypto/sql_store.go b/crypto/sql_store.go index 00544a9b..d5c6c564 100644 --- a/crypto/sql_store.go +++ b/crypto/sql_store.go @@ -21,7 +21,7 @@ import ( "go.mau.fi/util/dbutil" "maunium.net/go/mautrix" - "maunium.net/go/mautrix/crypto/goolm/cipher" + "maunium.net/go/mautrix/crypto/goolm/libolmpickle" "maunium.net/go/mautrix/crypto/olm" "maunium.net/go/mautrix/crypto/sql_store_upgrade" "maunium.net/go/mautrix/event" @@ -906,7 +906,7 @@ func (store *SQLCryptoStore) DropSignaturesByKey(ctx context.Context, userID id. } func (store *SQLCryptoStore) PutSecret(ctx context.Context, name id.Secret, value string) error { - bytes, err := cipher.Pickle(store.PickleKey, []byte(value)) + bytes, err := libolmpickle.Pickle(store.PickleKey, []byte(value)) if err != nil { return err } @@ -925,7 +925,7 @@ func (store *SQLCryptoStore) GetSecret(ctx context.Context, name id.Secret) (val } else if err != nil { return "", err } - bytes, err = cipher.Unpickle(store.PickleKey, bytes) + bytes, err = libolmpickle.Unpickle(store.PickleKey, bytes) return string(bytes), err }