Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

client: add key backup functions #154

Merged
merged 14 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"go.mau.fi/util/retryafter"
"maunium.net/go/maulogger/v2/maulogadapt"

"maunium.net/go/mautrix/crypto/backup"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/pushrules"
Expand Down Expand Up @@ -1944,6 +1945,158 @@ func (cli *Client) GetKeyChanges(ctx context.Context, from, to string) (resp *Re
return
}

// GetKeyBackup retrieves the keys from the backup.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#get_matrixclientv3room_keyskeys
func (cli *Client) GetKeyBackup(ctx context.Context, version string) (resp *RespRoomKeys[backup.EncryptedSessionData[backup.MegolmSessionData]], err error) {
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys"}, map[string]string{
"version": version,
})
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}

// PutKeysInBackup stores several keys in the backup.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#put_matrixclientv3room_keyskeys
func (cli *Client) PutKeysInBackup(ctx context.Context, version string, req *ReqKeyBackup) (resp *RespRoomKeysUpdate, err error) {
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys"}, map[string]string{
"version": version,
})
_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, req, &resp)
return
}

// DeleteKeyBackup deletes all keys from the backup.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#delete_matrixclientv3room_keyskeys
func (cli *Client) DeleteKeyBackup(ctx context.Context, version string) (resp *RespRoomKeysUpdate, err error) {
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys"}, map[string]string{
"version": version,
})
_, err = cli.MakeRequest(ctx, http.MethodDelete, urlPath, nil, &resp)
return
}

// GetKeyBackupForRoom retrieves the keys from the backup for the given room.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#get_matrixclientv3room_keyskeysroomid
func (cli *Client) GetKeyBackupForRoom(
ctx context.Context, version string, roomID id.RoomID,
) (resp *RespRoomKeyBackup[backup.EncryptedSessionData[backup.MegolmSessionData]], err error) {
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys", roomID.String()}, map[string]string{
"version": version,
})
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}

// PutKeysInBackupForRoom stores several keys in the backup for the given room.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#put_matrixclientv3room_keyskeysroomid
func (cli *Client) PutKeysInBackupForRoom(ctx context.Context, version string, roomID id.RoomID, req *ReqRoomKeyBackup) (resp *RespRoomKeysUpdate, err error) {
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys", roomID.String()}, map[string]string{
"version": version,
})
_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, req, &resp)
return
}

// DeleteKeysFromBackupForRoom deletes all the keys in the backup for the given
// room.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#delete_matrixclientv3room_keyskeysroomid
func (cli *Client) DeleteKeysFromBackupForRoom(ctx context.Context, version string, roomID id.RoomID) (resp *RespRoomKeysUpdate, err error) {
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys", roomID.String()}, map[string]string{
"version": version,
})
_, err = cli.MakeRequest(ctx, http.MethodDelete, urlPath, nil, &resp)
return
}

// GetKeyBackupForRoomAndSession retrieves a key from the backup.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#get_matrixclientv3room_keyskeysroomidsessionid
func (cli *Client) GetKeyBackupForRoomAndSession(
ctx context.Context, version string, roomID id.RoomID, sessionID id.SessionID,
) (resp *RespKeyBackupData[backup.EncryptedSessionData[backup.MegolmSessionData]], err error) {
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys", roomID.String(), sessionID.String()}, map[string]string{
"version": version,
})
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}

// PutKeysInBackupForRoomAndSession stores a key in the backup.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#put_matrixclientv3room_keyskeysroomidsessionid
func (cli *Client) PutKeysInBackupForRoomAndSession(ctx context.Context, version string, roomID id.RoomID, sessionID id.SessionID, req *ReqKeyBackupData) (resp *RespRoomKeysUpdate, err error) {
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys", roomID.String(), sessionID.String()}, map[string]string{
"version": version,
})
_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, req, &resp)
return
}

// DeleteKeysInBackupForRoomAndSession deletes a key from the backup.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#delete_matrixclientv3room_keyskeysroomidsessionid
func (cli *Client) DeleteKeysInBackupForRoomAndSession(ctx context.Context, version string, roomID id.RoomID, sessionID id.SessionID) (resp *RespRoomKeysUpdate, err error) {
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys", roomID.String(), sessionID.String()}, map[string]string{
"version": version,
})
_, err = cli.MakeRequest(ctx, http.MethodDelete, urlPath, nil, &resp)
return
}

// GetKeyBackupLatestVersion returns information about the latest backup version.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#get_matrixclientv3room_keysversion
func (cli *Client) GetKeyBackupLatestVersion(ctx context.Context) (resp *RespRoomKeysVersion[backup.MegolmAuthData], err error) {
urlPath := cli.BuildClientURL("v3", "room_keys", "version")
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}

// CreateKeyBackupVersion creates a new key backup.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#post_matrixclientv3room_keysversion
func (cli *Client) CreateKeyBackupVersion(ctx context.Context, req *ReqRoomKeysVersionCreate) (resp *RespRoomKeysVersionCreate, err error) {
urlPath := cli.BuildClientURL("v3", "room_keys", "version")
_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, req, &resp)
return
}

// GetKeyBackupVersion returns information about an existing key backup.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#get_matrixclientv3room_keysversionversion
func (cli *Client) GetKeyBackupVersion(ctx context.Context, version string) (resp *RespRoomKeysVersion[backup.MegolmAuthData], err error) {
urlPath := cli.BuildClientURL("v3", "room_keys", "version", version)
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}

// UpdateKeyBackupVersion updates information about an existing key backup. Only
// the auth_data can be modified.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#put_matrixclientv3room_keysversionversion
func (cli *Client) UpdateKeyBackupVersion(ctx context.Context, version string, req *ReqRoomKeysVersionUpdate) error {
urlPath := cli.BuildClientURL("v3", "room_keys", "version", version)
_, err := cli.MakeRequest(ctx, http.MethodPut, urlPath, nil, nil)
return err
}

// DeleteKeyBackupVersion deletes an existing key backup. Both the information
// about the backup, as well as all key data related to the backup will be
// deleted.
//
// See: https://spec.matrix.org/v1.1/client-server-api/#delete_matrixclientv3room_keysversionversion
func (cli *Client) DeleteKeyBackupVersion(ctx context.Context, version string) error {
urlPath := cli.BuildClientURL("v3", "room_keys", "version", version)
_, err := cli.MakeRequest(ctx, http.MethodDelete, urlPath, nil, nil)
return err
}

func (cli *Client) SendToDevice(ctx context.Context, eventType event.Type, req *ReqSendToDevice) (resp *RespSendToDevice, err error) {
urlPath := cli.BuildClientURL("v3", "sendToDevice", eventType.String(), cli.TxnID())
_, err = cli.MakeRequest(ctx, "PUT", urlPath, req, &resp)
Expand Down
13 changes: 3 additions & 10 deletions crypto/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package crypto
import (
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/crypto/signatures"
"maunium.net/go/mautrix/id"
)

Expand Down Expand Up @@ -62,11 +63,7 @@ func (account *OlmAccount) getInitialKeys(userID id.UserID, deviceID id.DeviceID
panic(err)
}

deviceKeys.Signatures = mautrix.Signatures{
userID: {
id.NewKeyID(id.KeyAlgorithmEd25519, deviceID.String()): signature,
},
}
deviceKeys.Signatures = signatures.NewSingleSignature(userID, id.KeyAlgorithmEd25519, deviceID.String(), signature)
return deviceKeys
}

Expand All @@ -79,11 +76,7 @@ func (account *OlmAccount) getOneTimeKeys(userID id.UserID, deviceID id.DeviceID
for keyID, key := range account.Internal.OneTimeKeys() {
key := mautrix.OneTimeKey{Key: key}
signature, _ := account.Internal.SignJSON(key)
key.Signatures = mautrix.Signatures{
userID: {
id.NewKeyID(id.KeyAlgorithmEd25519, deviceID.String()): signature,
},
}
key.Signatures = signatures.NewSingleSignature(userID, id.KeyAlgorithmEd25519, deviceID.String(), signature)
key.IsSigned = true
oneTimeKeys[id.NewKeyID(id.KeyAlgorithmSignedCurve25519, keyID)] = key
}
Expand Down
54 changes: 54 additions & 0 deletions crypto/aescbc/aes_cbc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package aescbc

import (
"crypto/aes"
"crypto/cipher"

"maunium.net/go/mautrix/crypto/pkcs7"
)

// Encrypt encrypts the plaintext with the key and IV. The IV length must be
// equal to the AES block size.
//
// This function might mutate the plaintext.
func Encrypt(key, iv, plaintext []byte) ([]byte, error) {
if len(key) == 0 {
return nil, ErrNoKeyProvided
}
if len(iv) != aes.BlockSize {
return nil, ErrIVNotBlockSize
}
plaintext = pkcs7.Pad(plaintext, aes.BlockSize)

block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}

cipher.NewCBCEncrypter(block, iv).CryptBlocks(plaintext, plaintext)
return plaintext, nil
}

// Decrypt decrypts the ciphertext with the key and IV. The IV length must be
// equal to the block size.
//
// This function mutates the ciphertext.
func Decrypt(key, iv, ciphertext []byte) ([]byte, error) {
if len(key) == 0 {
return nil, ErrNoKeyProvided
}
if len(iv) != aes.BlockSize {
return nil, ErrIVNotBlockSize
}

block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(ciphertext) < aes.BlockSize {
return nil, ErrNotMultipleBlockSize
}

cipher.NewCBCDecrypter(block, iv).CryptBlocks(ciphertext, ciphertext)
return pkcs7.Unpad(ciphertext), nil
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package crypto_test
package aescbc_test

import (
"bytes"
"crypto/aes"
"crypto/rand"
"testing"

"maunium.net/go/mautrix/crypto/goolm/crypto"
"maunium.net/go/mautrix/crypto/aescbc"
)

func TestAESCBC(t *testing.T) {
Expand All @@ -30,11 +30,11 @@ func TestAESCBC(t *testing.T) {
plaintext = append(plaintext, []byte("-")...)
}

if ciphertext, err = crypto.AESCBCEncrypt(key, iv, plaintext); err != nil {
if ciphertext, err = aescbc.Encrypt(key, iv, plaintext); err != nil {
t.Fatal(err)
}

resultPlainText, err := crypto.AESCBCDecrypt(key, iv, ciphertext)
resultPlainText, err := aescbc.Decrypt(key, iv, ciphertext)
if err != nil {
t.Fatal(err)
}
Expand All @@ -54,15 +54,15 @@ func TestAESCBCCase1(t *testing.T) {
input := make([]byte, 16)
key := make([]byte, 32)
iv := make([]byte, aes.BlockSize)
encrypted, err := crypto.AESCBCEncrypt(key, iv, input)
encrypted, err := aescbc.Encrypt(key, iv, input)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(expected, encrypted) {
t.Fatalf("encrypted did not match expected:\n%v\n%v\n", encrypted, expected)
}

decrypted, err := crypto.AESCBCDecrypt(key, iv, encrypted)
decrypted, err := aescbc.Decrypt(key, iv, encrypted)
if err != nil {
t.Fatal(err)
}
Expand Down
9 changes: 9 additions & 0 deletions crypto/aescbc/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package aescbc

import "errors"

var (
ErrNoKeyProvided = errors.New("no key")
ErrIVNotBlockSize = errors.New("IV length does not match AES block size")
ErrNotMultipleBlockSize = errors.New("ciphertext length is not a multiple of the AES block size")
)
Loading