github.com/status-im/status-go@v1.1.0/protocol/encryption/x3dh.go (about) 1 package encryption 2 3 import ( 4 "crypto/ecdsa" 5 "errors" 6 "sort" 7 "strconv" 8 "time" 9 10 "github.com/status-im/status-go/eth-node/crypto" 11 "github.com/status-im/status-go/eth-node/crypto/ecies" 12 ) 13 14 const ( 15 // Shared secret key length 16 sskLen = 16 17 ) 18 19 func buildSignatureMaterial(bundle *Bundle) []byte { 20 signedPreKeys := bundle.GetSignedPreKeys() 21 timestamp := bundle.GetTimestamp() 22 var keys []string 23 24 for k := range signedPreKeys { 25 keys = append(keys, k) 26 } 27 var signatureMaterial []byte 28 29 sort.Strings(keys) 30 31 for _, installationID := range keys { 32 signedPreKey := signedPreKeys[installationID] 33 signatureMaterial = append(signatureMaterial, []byte(installationID)...) 34 signatureMaterial = append(signatureMaterial, signedPreKey.SignedPreKey...) 35 signatureMaterial = append(signatureMaterial, []byte(strconv.FormatUint(uint64(signedPreKey.Version), 10))...) 36 // We don't use timestamp in the signature if it's 0, for backward compatibility 37 } 38 39 if timestamp != 0 { 40 signatureMaterial = append(signatureMaterial, []byte(strconv.FormatInt(timestamp, 10))...) 41 } 42 43 return signatureMaterial 44 45 } 46 47 // SignBundle signs the bundle and refreshes the timestamps 48 func SignBundle(identity *ecdsa.PrivateKey, bundleContainer *BundleContainer) error { 49 bundleContainer.Bundle.Timestamp = time.Now().UnixNano() 50 signatureMaterial := buildSignatureMaterial(bundleContainer.GetBundle()) 51 52 signature, err := crypto.Sign(crypto.Keccak256(signatureMaterial), identity) 53 if err != nil { 54 return err 55 } 56 bundleContainer.Bundle.Signature = signature 57 return nil 58 } 59 60 // NewBundleContainer creates a new BundleContainer from an identity private key 61 func NewBundleContainer(identity *ecdsa.PrivateKey, installationID string) (*BundleContainer, error) { 62 preKey, err := crypto.GenerateKey() 63 if err != nil { 64 return nil, err 65 } 66 67 compressedPreKey := crypto.CompressPubkey(&preKey.PublicKey) 68 compressedIdentityKey := crypto.CompressPubkey(&identity.PublicKey) 69 70 encodedPreKey := crypto.FromECDSA(preKey) 71 signedPreKeys := make(map[string]*SignedPreKey) 72 signedPreKeys[installationID] = &SignedPreKey{ 73 ProtocolVersion: protocolVersion, 74 SignedPreKey: compressedPreKey, 75 } 76 77 bundle := Bundle{ 78 Timestamp: time.Now().UnixNano(), 79 Identity: compressedIdentityKey, 80 SignedPreKeys: signedPreKeys, 81 } 82 83 return &BundleContainer{ 84 Bundle: &bundle, 85 PrivateSignedPreKey: encodedPreKey, 86 }, nil 87 } 88 89 // VerifyBundle checks that a bundle is valid 90 func VerifyBundle(bundle *Bundle) error { 91 _, err := ExtractIdentity(bundle) 92 return err 93 } 94 95 // ExtractIdentity extracts the identity key from a given bundle 96 func ExtractIdentity(bundle *Bundle) (*ecdsa.PublicKey, error) { 97 bundleIdentityKey, err := crypto.DecompressPubkey(bundle.GetIdentity()) 98 if err != nil { 99 return nil, err 100 } 101 102 signatureMaterial := buildSignatureMaterial(bundle) 103 104 recoveredKey, err := crypto.SigToPub( 105 crypto.Keccak256(signatureMaterial), 106 bundle.GetSignature(), 107 ) 108 if err != nil { 109 return nil, err 110 } 111 112 if crypto.PubkeyToAddress(*recoveredKey) != crypto.PubkeyToAddress(*bundleIdentityKey) { 113 return nil, errors.New("identity key and signature mismatch") 114 } 115 116 return recoveredKey, nil 117 } 118 119 // PerformDH generates a shared key given a private and a public key 120 func PerformDH(privateKey *ecies.PrivateKey, publicKey *ecies.PublicKey) ([]byte, error) { 121 return privateKey.GenerateShared( 122 publicKey, 123 sskLen, 124 sskLen, 125 ) 126 } 127 128 func getSharedSecret(dh1 []byte, dh2 []byte, dh3 []byte) []byte { 129 secretInput := append(append(dh1, dh2...), dh3...) 130 131 return crypto.Keccak256(secretInput) 132 } 133 134 // x3dhActive handles initiating an X3DH session 135 func x3dhActive( 136 myIdentityKey *ecies.PrivateKey, 137 theirSignedPreKey *ecies.PublicKey, 138 myEphemeralKey *ecies.PrivateKey, 139 theirIdentityKey *ecies.PublicKey, 140 ) ([]byte, error) { 141 var dh1, dh2, dh3 []byte 142 var err error 143 144 if dh1, err = PerformDH(myIdentityKey, theirSignedPreKey); err != nil { 145 return nil, err 146 } 147 148 if dh2, err = PerformDH(myEphemeralKey, theirIdentityKey); err != nil { 149 return nil, err 150 } 151 152 if dh3, err = PerformDH(myEphemeralKey, theirSignedPreKey); err != nil { 153 return nil, err 154 } 155 156 return getSharedSecret(dh1, dh2, dh3), nil 157 } 158 159 // x3dhPassive handles the response to an initiated X3DH session 160 func x3dhPassive( 161 theirIdentityKey *ecies.PublicKey, 162 mySignedPreKey *ecies.PrivateKey, 163 theirEphemeralKey *ecies.PublicKey, 164 myIdentityKey *ecies.PrivateKey, 165 ) ([]byte, error) { 166 var dh1, dh2, dh3 []byte 167 var err error 168 169 if dh1, err = PerformDH(mySignedPreKey, theirIdentityKey); err != nil { 170 return nil, err 171 } 172 173 if dh2, err = PerformDH(myIdentityKey, theirEphemeralKey); err != nil { 174 return nil, err 175 } 176 177 if dh3, err = PerformDH(mySignedPreKey, theirEphemeralKey); err != nil { 178 return nil, err 179 } 180 181 return getSharedSecret(dh1, dh2, dh3), nil 182 } 183 184 // PerformActiveDH performs a Diffie-Hellman exchange using a public key and a generated ephemeral key. 185 // Returns the key resulting from the DH exchange as well as the ephemeral public key. 186 func PerformActiveDH(publicKey *ecdsa.PublicKey) ([]byte, *ecdsa.PublicKey, error) { 187 ephemeralKey, err := crypto.GenerateKey() 188 if err != nil { 189 return nil, nil, err 190 } 191 192 key, err := PerformDH( 193 ecies.ImportECDSA(ephemeralKey), 194 ecies.ImportECDSAPublic(publicKey), 195 ) 196 if err != nil { 197 return nil, nil, err 198 } 199 200 return key, &ephemeralKey.PublicKey, err 201 } 202 203 // PerformActiveX3DH takes someone else's bundle and calculates shared secret. 204 // Returns the shared secret and the ephemeral key used. 205 func PerformActiveX3DH(identity []byte, signedPreKey []byte, prv *ecdsa.PrivateKey) ([]byte, *ecdsa.PublicKey, error) { 206 bundleIdentityKey, err := crypto.DecompressPubkey(identity) 207 if err != nil { 208 return nil, nil, err 209 } 210 211 bundleSignedPreKey, err := crypto.DecompressPubkey(signedPreKey) 212 if err != nil { 213 return nil, nil, err 214 } 215 216 ephemeralKey, err := crypto.GenerateKey() 217 if err != nil { 218 return nil, nil, err 219 } 220 221 sharedSecret, err := x3dhActive( 222 ecies.ImportECDSA(prv), 223 ecies.ImportECDSAPublic(bundleSignedPreKey), 224 ecies.ImportECDSA(ephemeralKey), 225 ecies.ImportECDSAPublic(bundleIdentityKey), 226 ) 227 if err != nil { 228 return nil, nil, err 229 } 230 231 return sharedSecret, &ephemeralKey.PublicKey, nil 232 } 233 234 // PerformPassiveX3DH handles the part of the protocol where 235 // our interlocutor used our bundle, with ID of the signedPreKey, 236 // we loaded our identity key and the correct signedPreKey and we perform X3DH 237 func PerformPassiveX3DH(theirIdentityKey *ecdsa.PublicKey, mySignedPreKey *ecdsa.PrivateKey, theirEphemeralKey *ecdsa.PublicKey, myPrivateKey *ecdsa.PrivateKey) ([]byte, error) { 238 sharedSecret, err := x3dhPassive( 239 ecies.ImportECDSAPublic(theirIdentityKey), 240 ecies.ImportECDSA(mySignedPreKey), 241 ecies.ImportECDSAPublic(theirEphemeralKey), 242 ecies.ImportECDSA(myPrivateKey), 243 ) 244 if err != nil { 245 return nil, err 246 } 247 248 return sharedSecret, nil 249 }