249 lines
6.7 KiB
Go
249 lines
6.7 KiB
Go
package chat
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"errors"
|
|
"sort"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/crypto"
|
|
"github.com/ethereum/go-ethereum/crypto/ecies"
|
|
"github.com/status-im/status-go/messaging/chat/protobuf"
|
|
)
|
|
|
|
const (
|
|
// Shared secret key length
|
|
sskLen = 16
|
|
)
|
|
|
|
func buildSignatureMaterial(bundle *protobuf.Bundle) []byte {
|
|
signedPreKeys := bundle.GetSignedPreKeys()
|
|
timestamp := bundle.GetTimestamp()
|
|
var keys []string
|
|
|
|
for k := range signedPreKeys {
|
|
keys = append(keys, k)
|
|
}
|
|
var signatureMaterial []byte
|
|
|
|
sort.Strings(keys)
|
|
|
|
for _, installationID := range keys {
|
|
signedPreKey := signedPreKeys[installationID]
|
|
signatureMaterial = append(signatureMaterial, []byte(installationID)...)
|
|
signatureMaterial = append(signatureMaterial, signedPreKey.SignedPreKey...)
|
|
signatureMaterial = append(signatureMaterial, []byte(strconv.FormatUint(uint64(signedPreKey.Version), 10))...)
|
|
// We don't use timestamp in the signature if it's 0, for backward compatibility
|
|
}
|
|
|
|
if timestamp != 0 {
|
|
signatureMaterial = append(signatureMaterial, []byte(strconv.FormatInt(timestamp, 10))...)
|
|
}
|
|
|
|
return signatureMaterial
|
|
|
|
}
|
|
|
|
func SignBundle(identity *ecdsa.PrivateKey, bundleContainer *protobuf.BundleContainer) error {
|
|
signatureMaterial := buildSignatureMaterial(bundleContainer.GetBundle())
|
|
|
|
signature, err := crypto.Sign(crypto.Keccak256(signatureMaterial), identity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bundleContainer.Bundle.Signature = signature
|
|
return nil
|
|
}
|
|
|
|
// NewBundleContainer creates a new BundleContainer from an identity private key
|
|
func NewBundleContainer(identity *ecdsa.PrivateKey, installationID string) (*protobuf.BundleContainer, error) {
|
|
preKey, err := crypto.GenerateKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
compressedPreKey := crypto.CompressPubkey(&preKey.PublicKey)
|
|
compressedIdentityKey := crypto.CompressPubkey(&identity.PublicKey)
|
|
|
|
encodedPreKey := crypto.FromECDSA(preKey)
|
|
signedPreKeys := make(map[string]*protobuf.SignedPreKey)
|
|
signedPreKeys[installationID] = &protobuf.SignedPreKey{
|
|
ProtocolVersion: ProtocolVersion,
|
|
SignedPreKey: compressedPreKey,
|
|
}
|
|
|
|
bundle := protobuf.Bundle{
|
|
Timestamp: time.Now().UnixNano(),
|
|
Identity: compressedIdentityKey,
|
|
SignedPreKeys: signedPreKeys,
|
|
}
|
|
|
|
return &protobuf.BundleContainer{
|
|
Bundle: &bundle,
|
|
PrivateSignedPreKey: encodedPreKey,
|
|
}, nil
|
|
}
|
|
|
|
// VerifyBundle checks that a bundle is valid
|
|
func VerifyBundle(bundle *protobuf.Bundle) error {
|
|
_, err := ExtractIdentity(bundle)
|
|
return err
|
|
}
|
|
|
|
// ExtractIdentity extracts the identity key from a given bundle
|
|
func ExtractIdentity(bundle *protobuf.Bundle) (*ecdsa.PublicKey, error) {
|
|
bundleIdentityKey, err := crypto.DecompressPubkey(bundle.GetIdentity())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
signatureMaterial := buildSignatureMaterial(bundle)
|
|
|
|
recoveredKey, err := crypto.SigToPub(
|
|
crypto.Keccak256(signatureMaterial),
|
|
bundle.GetSignature(),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if crypto.PubkeyToAddress(*recoveredKey) != crypto.PubkeyToAddress(*bundleIdentityKey) {
|
|
return nil, errors.New("identity key and signature mismatch")
|
|
}
|
|
|
|
return recoveredKey, nil
|
|
}
|
|
|
|
// PerformDH generates a shared key given a private and a public key
|
|
func PerformDH(privateKey *ecies.PrivateKey, publicKey *ecies.PublicKey) ([]byte, error) {
|
|
return privateKey.GenerateShared(
|
|
publicKey,
|
|
sskLen,
|
|
sskLen,
|
|
)
|
|
}
|
|
|
|
func getSharedSecret(dh1 []byte, dh2 []byte, dh3 []byte) []byte {
|
|
secretInput := append(append(dh1, dh2...), dh3...)
|
|
|
|
return crypto.Keccak256(secretInput)
|
|
}
|
|
|
|
// x3dhActive handles initiating an X3DH session
|
|
func x3dhActive(
|
|
myIdentityKey *ecies.PrivateKey,
|
|
theirSignedPreKey *ecies.PublicKey,
|
|
myEphemeralKey *ecies.PrivateKey,
|
|
theirIdentityKey *ecies.PublicKey,
|
|
) ([]byte, error) {
|
|
var dh1, dh2, dh3 []byte
|
|
var err error
|
|
|
|
if dh1, err = PerformDH(myIdentityKey, theirSignedPreKey); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if dh2, err = PerformDH(myEphemeralKey, theirIdentityKey); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if dh3, err = PerformDH(myEphemeralKey, theirSignedPreKey); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return getSharedSecret(dh1, dh2, dh3), nil
|
|
}
|
|
|
|
// x3dhPassive handles the response to an initiated X3DH session
|
|
func x3dhPassive(
|
|
theirIdentityKey *ecies.PublicKey,
|
|
mySignedPreKey *ecies.PrivateKey,
|
|
theirEphemeralKey *ecies.PublicKey,
|
|
myIdentityKey *ecies.PrivateKey,
|
|
) ([]byte, error) {
|
|
var dh1, dh2, dh3 []byte
|
|
var err error
|
|
|
|
if dh1, err = PerformDH(mySignedPreKey, theirIdentityKey); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if dh2, err = PerformDH(myIdentityKey, theirEphemeralKey); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if dh3, err = PerformDH(mySignedPreKey, theirEphemeralKey); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return getSharedSecret(dh1, dh2, dh3), nil
|
|
}
|
|
|
|
// PerformActiveDH performs a Diffie-Hellman exchange using a public key and a generated ephemeral key.
|
|
// Returns the key resulting from the DH exchange as well as the ephemeral public key.
|
|
func PerformActiveDH(publicKey *ecdsa.PublicKey) ([]byte, *ecdsa.PublicKey, error) {
|
|
ephemeralKey, err := crypto.GenerateKey()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
key, err := PerformDH(
|
|
ecies.ImportECDSA(ephemeralKey),
|
|
ecies.ImportECDSAPublic(publicKey),
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return key, &ephemeralKey.PublicKey, err
|
|
}
|
|
|
|
// PerformActiveX3DH takes someone else's bundle and calculates shared secret.
|
|
// Returns the shared secret and the ephemeral key used.
|
|
func PerformActiveX3DH(identity []byte, signedPreKey []byte, prv *ecdsa.PrivateKey) ([]byte, *ecdsa.PublicKey, error) {
|
|
bundleIdentityKey, err := crypto.DecompressPubkey(identity)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
bundleSignedPreKey, err := crypto.DecompressPubkey(signedPreKey)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
ephemeralKey, err := crypto.GenerateKey()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
sharedSecret, err := x3dhActive(
|
|
ecies.ImportECDSA(prv),
|
|
ecies.ImportECDSAPublic(bundleSignedPreKey),
|
|
ecies.ImportECDSA(ephemeralKey),
|
|
ecies.ImportECDSAPublic(bundleIdentityKey),
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return sharedSecret, &ephemeralKey.PublicKey, nil
|
|
}
|
|
|
|
// PerformPassiveX3DH handles the part of the protocol where
|
|
// our interlocutor used our bundle, with ID of the signedPreKey,
|
|
// we loaded our identity key and the correct signedPreKey and we perform X3DH
|
|
func PerformPassiveX3DH(theirIdentityKey *ecdsa.PublicKey, mySignedPreKey *ecdsa.PrivateKey, theirEphemeralKey *ecdsa.PublicKey, myPrivateKey *ecdsa.PrivateKey) ([]byte, error) {
|
|
sharedSecret, err := x3dhPassive(
|
|
ecies.ImportECDSAPublic(theirIdentityKey),
|
|
ecies.ImportECDSA(mySignedPreKey),
|
|
ecies.ImportECDSAPublic(theirEphemeralKey),
|
|
ecies.ImportECDSA(myPrivateKey),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return sharedSecret, nil
|
|
}
|