mautrix-signal/pkg/signalmeow/devicename.go

142 lines
4.8 KiB
Go

// mautrix-signal - A Matrix-signal puppeting bridge.
// Copyright (C) 2023 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package signalmeow
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/sha256"
"encoding/json"
"fmt"
"net/http"
"google.golang.org/protobuf/proto"
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
"go.mau.fi/mautrix-signal/pkg/signalmeow/web"
)
func hmacSHA256(key, input []byte) []byte {
hash := hmac.New(sha256.New, key)
hash.Write(input)
return hash.Sum(nil)
}
func aes256CTR(key, iv, dst, source []byte) {
block, _ := aes.NewCipher(key)
cipher.NewCTR(block, iv).XORKeyStream(dst, source)
}
func (cli *Client) UpdateDeviceName(ctx context.Context, name string) error {
encryptedName, err := EncryptDeviceName(name, cli.Store.ACIIdentityKeyPair.GetPublicKey())
if err != nil {
return fmt.Errorf("failed to encrypt device name: %w", err)
}
err = cli.updateDeviceName(ctx, encryptedName)
if err != nil {
return fmt.Errorf("failed to update device name: %w", err)
}
return nil
}
func (cli *Client) updateDeviceName(ctx context.Context, encryptedName []byte) error {
reqData, err := json.Marshal(map[string]any{
"deviceName": encryptedName,
})
if err != nil {
return fmt.Errorf("failed to marshal device name update request: %w", err)
}
username, password := cli.Store.BasicAuthCreds()
resp, err := web.SendHTTPRequest(ctx, http.MethodPut, "/v1/accounts/name", &web.HTTPReqOpt{
Body: reqData,
Username: &username,
Password: &password,
})
if err != nil {
return fmt.Errorf("failed to send device name update request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("device name update request returned status %d", resp.StatusCode)
}
return nil
}
func EncryptDeviceName(name string, identityKey *libsignalgo.PublicKey) ([]byte, error) {
ephemeralPrivKey, err := libsignalgo.GeneratePrivateKey()
if err != nil {
return nil, fmt.Errorf("failed to generate ephemeral private key: %w", err)
}
ephemeralPubKey, err := ephemeralPrivKey.GetPublicKey()
if err != nil {
return nil, fmt.Errorf("failed to generate ephemeral public key: %w", err)
}
ephemeralPubKeyBytes, err := ephemeralPubKey.Serialize()
if err != nil {
return nil, fmt.Errorf("failed to serialize ephemeral public key: %w", err)
}
masterSecret, err := ephemeralPrivKey.Agree(identityKey)
if err != nil {
return nil, fmt.Errorf("failed to agree on master secret: %w", err)
}
nameBytes := []byte(name)
key1 := hmacSHA256(masterSecret, []byte("auth"))
syntheticIV := hmacSHA256(key1, nameBytes)[:16]
key2 := hmacSHA256(masterSecret, []byte("cipher"))
cipherKey := hmacSHA256(key2, syntheticIV)
aes256CTR(cipherKey, make([]byte, 16), nameBytes, nameBytes)
wrappedData, err := proto.Marshal(&signalpb.DeviceName{
EphemeralPublic: ephemeralPubKeyBytes,
SyntheticIv: syntheticIV,
Ciphertext: nameBytes,
})
if err != nil {
return nil, fmt.Errorf("failed to marshal encrypted device name protobuf: %w", err)
}
return wrappedData, nil
}
func DecryptDeviceName(wrappedData []byte, identityKey *libsignalgo.PrivateKey) (string, error) {
var name signalpb.DeviceName
err := proto.Unmarshal(wrappedData, &name)
if err != nil {
return "", fmt.Errorf("failed to unmarshal encrypted device name protobuf: %w", err)
}
ephemeralPubKey, err := libsignalgo.DeserializePublicKey(name.EphemeralPublic)
if err != nil {
return "", fmt.Errorf("failed to deserialize ephemeral public key: %w", err)
}
masterSecret, err := identityKey.Agree(ephemeralPubKey)
if err != nil {
return "", fmt.Errorf("failed to agree on master secret: %w", err)
}
key2 := hmacSHA256(masterSecret, []byte("cipher"))
cipherKey := hmacSHA256(key2, name.SyntheticIv)
decryptedName := make([]byte, len(name.Ciphertext))
aes256CTR(cipherKey, make([]byte, 16), decryptedName, name.Ciphertext)
key1 := hmacSHA256(masterSecret, []byte("auth"))
syntheticIV := hmacSHA256(key1, decryptedName)[:16]
if !hmac.Equal(key1, hmacSHA256(name.SyntheticIv, syntheticIV)) {
return "", fmt.Errorf("mismatching synthetic IV")
}
return string(decryptedName), nil
}