// Copyright (c) 2020 Nikos Filippakis // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. //go:build !nosas // +build !nosas package crypto import ( "context" "encoding/json" "errors" "fmt" "math/rand" "sort" "strconv" "strings" "sync" "time" "maunium.net/go/mautrix" "maunium.net/go/mautrix/crypto/canonicaljson" "maunium.net/go/mautrix/crypto/olm" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) var ( ErrUnknownUserForTransaction = errors.New("unknown user for transaction") ErrTransactionAlreadyExists = errors.New("transaction already exists") // ErrUnknownTransaction is returned when a key verification message is received with an unknown transaction ID. ErrUnknownTransaction = errors.New("unknown transaction") // ErrUnknownVerificationMethod is returned when the verification method in a received m.key.verification.start is unknown. ErrUnknownVerificationMethod = errors.New("unknown verification method") ) type VerificationHooks interface { // VerifySASMatch receives the generated SAS and its method, as well as the device that is being verified. // It returns whether the given SAS match with the SAS displayed on other device. VerifySASMatch(otherDevice *DeviceIdentity, sas SASData) bool // VerificationMethods returns the list of supported verification methods in order of preference. // It must contain at least the decimal method. VerificationMethods() []VerificationMethod OnCancel(cancelledByUs bool, reason string, reasonCode event.VerificationCancelCode) OnSuccess() } type VerificationRequestResponse int const ( AcceptRequest VerificationRequestResponse = iota RejectRequest IgnoreRequest ) // sendToOneDevice sends a to-device event to a single device. func (mach *OlmMachine) sendToOneDevice(userID id.UserID, deviceID id.DeviceID, eventType event.Type, content interface{}) error { _, err := mach.Client.SendToDevice(eventType, &mautrix.ReqSendToDevice{ Messages: map[id.UserID]map[id.DeviceID]*event.Content{ userID: { deviceID: { Parsed: content, }, }, }, }) return err } func (mach *OlmMachine) getPKAndKeysMAC(sas *olm.SAS, sendingUser id.UserID, sendingDevice id.DeviceID, receivingUser id.UserID, receivingDevice id.DeviceID, transactionID string, signingKey id.SigningKey, mainKeyID id.KeyID, keys map[id.KeyID]string) (string, string, error) { sasInfo := "MATRIX_KEY_VERIFICATION_MAC" + sendingUser.String() + sendingDevice.String() + receivingUser.String() + receivingDevice.String() + transactionID // get key IDs from key map keyIDStrings := make([]string, len(keys)) i := 0 for keyID := range keys { keyIDStrings[i] = keyID.String() i++ } sort.Sort(sort.StringSlice(keyIDStrings)) keyIDString := strings.Join(keyIDStrings, ",") pubKeyMac, err := sas.CalculateMAC([]byte(signingKey), []byte(sasInfo+mainKeyID.String())) if err != nil { return "", "", err } mach.Log.Trace("sas.CalculateMAC(\"%s\", \"%s\") -> \"%s\"", signingKey, sasInfo+mainKeyID.String(), string(pubKeyMac)) keysMac, err := sas.CalculateMAC([]byte(keyIDString), []byte(sasInfo+"KEY_IDS")) if err != nil { return "", "", err } mach.Log.Trace("sas.CalculateMAC(\"%s\", \"%s\") -> \"%s\"", keyIDString, sasInfo+"KEY_IDS", string(keysMac)) return string(pubKeyMac), string(keysMac), nil } // verificationState holds all the information needed for the state of a SAS verification with another device. type verificationState struct { sas *olm.SAS otherDevice *DeviceIdentity initiatedByUs bool verificationStarted bool keyReceived bool sasMatched chan bool commitment string startEventCanonical string chosenSASMethod VerificationMethod hooks VerificationHooks extendTimeout context.CancelFunc inRoomID id.RoomID lock sync.Mutex } // getTransactionState retrieves the given transaction's state, or cancels the transaction if it cannot be found or there is a mismatch. func (mach *OlmMachine) getTransactionState(transactionID string, userID id.UserID) (*verificationState, error) { verStateInterface, ok := mach.keyVerificationTransactionState.Load(userID.String() + ":" + transactionID) if !ok { _ = mach.SendSASVerificationCancel(userID, id.DeviceID("*"), transactionID, "Unknown transaction: "+transactionID, event.VerificationCancelUnknownTransaction) return nil, ErrUnknownTransaction } verState := verStateInterface.(*verificationState) if verState.otherDevice.UserID != userID { reason := fmt.Sprintf("Unknown user for transaction %v: %v", transactionID, userID) if verState.inRoomID == "" { _ = mach.SendSASVerificationCancel(userID, id.DeviceID("*"), transactionID, reason, event.VerificationCancelUserMismatch) } else { _ = mach.SendInRoomSASVerificationCancel(verState.inRoomID, userID, transactionID, reason, event.VerificationCancelUserMismatch) } mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID) return nil, fmt.Errorf("%w %s: %s", ErrUnknownUserForTransaction, transactionID, userID) } return verState, nil } // handleVerificationStart handles an incoming m.key.verification.start message. // It initializes the state for this SAS verification process and stores it. func (mach *OlmMachine) handleVerificationStart(userID id.UserID, content *event.VerificationStartEventContent, transactionID string, timeout time.Duration, inRoomID id.RoomID) { mach.Log.Debug("Received verification start from %v", content.FromDevice) otherDevice, err := mach.GetOrFetchDevice(userID, content.FromDevice) if err != nil { mach.Log.Error("Could not find device %v of user %v", content.FromDevice, userID) return } warnAndCancel := func(logReason, cancelReason string) { mach.Log.Warn("Canceling verification transaction %v as it %s", transactionID, logReason) if inRoomID == "" { _ = mach.SendSASVerificationCancel(otherDevice.UserID, otherDevice.DeviceID, transactionID, cancelReason, event.VerificationCancelUnknownMethod) } else { _ = mach.SendInRoomSASVerificationCancel(inRoomID, otherDevice.UserID, transactionID, cancelReason, event.VerificationCancelUnknownMethod) } } switch { case content.Method != event.VerificationMethodSAS: warnAndCancel("is not SAS", "Only SAS method is supported") case !content.SupportsKeyAgreementProtocol(event.KeyAgreementCurve25519HKDFSHA256): warnAndCancel("does not support key agreement protocol curve25519-hkdf-sha256", "Only curve25519-hkdf-sha256 key agreement protocol is supported") case !content.SupportsHashMethod(event.VerificationHashSHA256): warnAndCancel("does not support SHA256 hashing", "Only SHA256 hashing is supported") case !content.SupportsMACMethod(event.HKDFHMACSHA256): warnAndCancel("does not support MAC method hkdf-hmac-sha256", "Only hkdf-hmac-sha256 MAC method is supported") case !content.SupportsSASMethod(event.SASDecimal): warnAndCancel("does not support decimal SAS", "Decimal SAS method must be supported") default: mach.actuallyStartVerification(userID, content, otherDevice, transactionID, timeout, inRoomID) } } func (mach *OlmMachine) actuallyStartVerification(userID id.UserID, content *event.VerificationStartEventContent, otherDevice *DeviceIdentity, transactionID string, timeout time.Duration, inRoomID id.RoomID) { if inRoomID != "" && transactionID != "" { verState, err := mach.getTransactionState(transactionID, userID) if err != nil { mach.Log.Error("Failed to get transaction state for in-room verification %s start: %v", transactionID, err) _ = mach.SendInRoomSASVerificationCancel(inRoomID, otherDevice.UserID, transactionID, "Internal state error in gomuks :(", "net.maunium.internal_error") return } mach.timeoutAfter(verState, transactionID, timeout) sasMethods := commonSASMethods(verState.hooks, content.ShortAuthenticationString) err = mach.SendInRoomSASVerificationAccept(inRoomID, userID, content, transactionID, verState.sas.GetPubkey(), sasMethods) if err != nil { mach.Log.Error("Error accepting in-room SAS verification: %v", err) } verState.chosenSASMethod = sasMethods[0] verState.verificationStarted = true return } resp, hooks := mach.AcceptVerificationFrom(transactionID, otherDevice, inRoomID) if resp == AcceptRequest { sasMethods := commonSASMethods(hooks, content.ShortAuthenticationString) if len(sasMethods) == 0 { mach.Log.Error("No common SAS methods: %v", content.ShortAuthenticationString) if inRoomID == "" { _ = mach.SendSASVerificationCancel(otherDevice.UserID, otherDevice.DeviceID, transactionID, "No common SAS methods", event.VerificationCancelUnknownMethod) } else { _ = mach.SendInRoomSASVerificationCancel(inRoomID, otherDevice.UserID, transactionID, "No common SAS methods", event.VerificationCancelUnknownMethod) } return } verState := &verificationState{ sas: olm.NewSAS(), otherDevice: otherDevice, initiatedByUs: false, verificationStarted: true, keyReceived: false, sasMatched: make(chan bool, 1), hooks: hooks, chosenSASMethod: sasMethods[0], inRoomID: inRoomID, } verState.lock.Lock() defer verState.lock.Unlock() _, loaded := mach.keyVerificationTransactionState.LoadOrStore(userID.String()+":"+transactionID, verState) if loaded { // transaction already exists mach.Log.Error("Transaction %v already exists, canceling", transactionID) if inRoomID == "" { _ = mach.SendSASVerificationCancel(otherDevice.UserID, otherDevice.DeviceID, transactionID, "Transaction already exists", event.VerificationCancelUnexpectedMessage) } else { _ = mach.SendInRoomSASVerificationCancel(inRoomID, otherDevice.UserID, transactionID, "Transaction already exists", event.VerificationCancelUnexpectedMessage) } return } mach.timeoutAfter(verState, transactionID, timeout) var err error if inRoomID == "" { err = mach.SendSASVerificationAccept(userID, content, verState.sas.GetPubkey(), sasMethods) } else { err = mach.SendInRoomSASVerificationAccept(inRoomID, userID, content, transactionID, verState.sas.GetPubkey(), sasMethods) } if err != nil { mach.Log.Error("Error accepting SAS verification: %v", err) } } else if resp == RejectRequest { mach.Log.Debug("Not accepting SAS verification %v from %v of user %v", transactionID, otherDevice.DeviceID, otherDevice.UserID) var err error if inRoomID == "" { err = mach.SendSASVerificationCancel(otherDevice.UserID, otherDevice.DeviceID, transactionID, "Not accepted by user", event.VerificationCancelByUser) } else { err = mach.SendInRoomSASVerificationCancel(inRoomID, otherDevice.UserID, transactionID, "Not accepted by user", event.VerificationCancelByUser) } if err != nil { mach.Log.Error("Error canceling SAS verification: %v", err) } } else { mach.Log.Debug("Ignoring SAS verification %v from %v of user %v", transactionID, otherDevice.DeviceID, otherDevice.UserID) } } func (mach *OlmMachine) timeoutAfter(verState *verificationState, transactionID string, timeout time.Duration) { timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), timeout) verState.extendTimeout = timeoutCancel go func() { mapKey := verState.otherDevice.UserID.String() + ":" + transactionID for { <-timeoutCtx.Done() // when timeout context is done verState.lock.Lock() // if transaction not active anymore, return if _, ok := mach.keyVerificationTransactionState.Load(mapKey); !ok { verState.lock.Unlock() return } if timeoutCtx.Err() == context.DeadlineExceeded { // if deadline exceeded cancel due to timeout mach.keyVerificationTransactionState.Delete(mapKey) _ = mach.callbackAndCancelSASVerification(verState, transactionID, "Timed out", event.VerificationCancelByTimeout) mach.Log.Warn("Verification transaction %v is canceled due to timing out", transactionID) verState.lock.Unlock() return } // otherwise the cancel func was called, so the timeout is reset mach.Log.Debug("Extending timeout for transaction %v", transactionID) timeoutCtx, timeoutCancel = context.WithTimeout(context.Background(), timeout) verState.extendTimeout = timeoutCancel verState.lock.Unlock() } }() } // handleVerificationAccept handles an incoming m.key.verification.accept message. // It continues the SAS verification process by sending the SAS key message to the other device. func (mach *OlmMachine) handleVerificationAccept(userID id.UserID, content *event.VerificationAcceptEventContent, transactionID string) { mach.Log.Debug("Received verification accept for transaction %v", transactionID) verState, err := mach.getTransactionState(transactionID, userID) if err != nil { mach.Log.Error("Error getting transaction state: %v", err) return } verState.lock.Lock() defer verState.lock.Unlock() verState.extendTimeout() if !verState.initiatedByUs || verState.verificationStarted { // unexpected accept at this point mach.Log.Warn("Unexpected verification accept message for transaction %v", transactionID) mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID) _ = mach.callbackAndCancelSASVerification(verState, transactionID, "Unexpected accept message", event.VerificationCancelUnexpectedMessage) return } sasMethods := commonSASMethods(verState.hooks, content.ShortAuthenticationString) if content.KeyAgreementProtocol != event.KeyAgreementCurve25519HKDFSHA256 || content.Hash != event.VerificationHashSHA256 || content.MessageAuthenticationCode != event.HKDFHMACSHA256 || len(sasMethods) == 0 { mach.Log.Warn("Canceling verification transaction %v due to unknown parameter", transactionID) mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID) _ = mach.callbackAndCancelSASVerification(verState, transactionID, "Verification uses unknown method", event.VerificationCancelUnknownMethod) return } key := verState.sas.GetPubkey() verState.commitment = content.Commitment verState.chosenSASMethod = sasMethods[0] verState.verificationStarted = true if verState.inRoomID == "" { err = mach.SendSASVerificationKey(userID, verState.otherDevice.DeviceID, transactionID, string(key)) } else { err = mach.SendInRoomSASVerificationKey(verState.inRoomID, userID, transactionID, string(key)) } if err != nil { mach.Log.Error("Error sending SAS key to other device: %v", err) return } } // handleVerificationKey handles an incoming m.key.verification.key message. // It stores the other device's public key in order to acquire the SAS shared secret. func (mach *OlmMachine) handleVerificationKey(userID id.UserID, content *event.VerificationKeyEventContent, transactionID string) { mach.Log.Debug("Got verification key for transaction %v: %v", transactionID, content.Key) verState, err := mach.getTransactionState(transactionID, userID) if err != nil { mach.Log.Error("Error getting transaction state: %v", err) return } verState.lock.Lock() defer verState.lock.Unlock() verState.extendTimeout() device := verState.otherDevice if !verState.verificationStarted || verState.keyReceived { // unexpected key at this point mach.Log.Warn("Unexpected verification key message for transaction %v", transactionID) mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID) _ = mach.callbackAndCancelSASVerification(verState, transactionID, "Unexpected key message", event.VerificationCancelUnexpectedMessage) return } if err := verState.sas.SetTheirKey([]byte(content.Key)); err != nil { mach.Log.Error("Error setting other device's key: %v", err) return } verState.keyReceived = true if verState.initiatedByUs { // verify commitment string from accept message now expectedCommitment := olm.NewUtility().Sha256(content.Key + verState.startEventCanonical) mach.Log.Debug("Received commitment: %v Expected: %v", verState.commitment, expectedCommitment) if expectedCommitment != verState.commitment { mach.Log.Warn("Canceling verification transaction %v due to commitment mismatch", transactionID) mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID) _ = mach.callbackAndCancelSASVerification(verState, transactionID, "Commitment mismatch", event.VerificationCancelCommitmentMismatch) return } } else { // if verification was initiated by other device, send out our key now key := verState.sas.GetPubkey() if verState.inRoomID == "" { err = mach.SendSASVerificationKey(userID, device.DeviceID, transactionID, string(key)) } else { err = mach.SendInRoomSASVerificationKey(verState.inRoomID, userID, transactionID, string(key)) } if err != nil { mach.Log.Error("Error sending SAS key to other device: %v", err) return } } // compare the SAS keys in a new goroutine and, when the verification is complete, send out the MAC var initUserID, acceptUserID id.UserID var initDeviceID, acceptDeviceID id.DeviceID var initKey, acceptKey string if verState.initiatedByUs { initUserID = mach.Client.UserID initDeviceID = mach.Client.DeviceID initKey = string(verState.sas.GetPubkey()) acceptUserID = device.UserID acceptDeviceID = device.DeviceID acceptKey = content.Key } else { initUserID = device.UserID initDeviceID = device.DeviceID initKey = content.Key acceptUserID = mach.Client.UserID acceptDeviceID = mach.Client.DeviceID acceptKey = string(verState.sas.GetPubkey()) } // use the prefered SAS method to generate a SAS sasMethod := verState.chosenSASMethod sas, err := sasMethod.GetVerificationSAS(initUserID, initDeviceID, initKey, acceptUserID, acceptDeviceID, acceptKey, transactionID, verState.sas) if err != nil { mach.Log.Error("Error generating SAS (method %v): %v", sasMethod.Type(), err) return } mach.Log.Debug("Generated SAS (%v): %v", sasMethod.Type(), sas) go func() { result := verState.hooks.VerifySASMatch(device, sas) mach.sasCompared(result, transactionID, verState) }() } // sasCompared is called asynchronously. It waits for the SAS to be compared for the verification to proceed. // If the SAS match, then our MAC is sent out. Otherwise the transaction is canceled. func (mach *OlmMachine) sasCompared(didMatch bool, transactionID string, verState *verificationState) { verState.lock.Lock() defer verState.lock.Unlock() verState.extendTimeout() if didMatch { verState.sasMatched <- true var err error if verState.inRoomID == "" { err = mach.SendSASVerificationMAC(verState.otherDevice.UserID, verState.otherDevice.DeviceID, transactionID, verState.sas) } else { err = mach.SendInRoomSASVerificationMAC(verState.inRoomID, verState.otherDevice.UserID, verState.otherDevice.DeviceID, transactionID, verState.sas) } if err != nil { mach.Log.Error("Error sending verification MAC to other device: %v", err) } } else { verState.sasMatched <- false } } // handleVerificationMAC handles an incoming m.key.verification.mac message. // It verifies the other device's MAC and if the MAC is valid it marks the device as trusted. func (mach *OlmMachine) handleVerificationMAC(userID id.UserID, content *event.VerificationMacEventContent, transactionID string) { mach.Log.Debug("Got MAC for verification %v: %v, MAC for keys: %v", transactionID, content.Mac, content.Keys) verState, err := mach.getTransactionState(transactionID, userID) if err != nil { mach.Log.Error("Error getting transaction state: %v", err) return } verState.lock.Lock() defer verState.lock.Unlock() verState.extendTimeout() device := verState.otherDevice // we are done with this SAS verification in all cases so we forget about it mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID) if !verState.verificationStarted || !verState.keyReceived { // unexpected MAC at this point mach.Log.Warn("Unexpected MAC message for transaction %v", transactionID) _ = mach.callbackAndCancelSASVerification(verState, transactionID, "Unexpected MAC message", event.VerificationCancelUnexpectedMessage) return } // do this in another goroutine as the match result might take a long time to arrive go func() { matched := <-verState.sasMatched verState.lock.Lock() defer verState.lock.Unlock() if !matched { mach.Log.Warn("SAS do not match! Canceling transaction %v", transactionID) _ = mach.callbackAndCancelSASVerification(verState, transactionID, "SAS do not match", event.VerificationCancelSASMismatch) return } keyID := id.NewKeyID(id.KeyAlgorithmEd25519, device.DeviceID.String()) expectedPKMAC, expectedKeysMAC, err := mach.getPKAndKeysMAC(verState.sas, device.UserID, device.DeviceID, mach.Client.UserID, mach.Client.DeviceID, transactionID, device.SigningKey, keyID, content.Mac) if err != nil { mach.Log.Error("Error generating MAC to match with received MAC: %v", err) return } mach.Log.Debug("Expected %s keys MAC, got %s", expectedKeysMAC, content.Keys) if content.Keys != expectedKeysMAC { mach.Log.Warn("Canceling verification transaction %v due to mismatched keys MAC", transactionID) _ = mach.callbackAndCancelSASVerification(verState, transactionID, "Mismatched keys MACs", event.VerificationCancelKeyMismatch) return } mach.Log.Debug("Expected %s PK MAC, got %s", expectedPKMAC, content.Mac[keyID]) if content.Mac[keyID] != expectedPKMAC { mach.Log.Warn("Canceling verification transaction %v due to mismatched PK MAC", transactionID) _ = mach.callbackAndCancelSASVerification(verState, transactionID, "Mismatched PK MACs", event.VerificationCancelKeyMismatch) return } // we can finally trust this device device.Trust = TrustStateVerified err = mach.CryptoStore.PutDevice(device.UserID, device) if err != nil { mach.Log.Warn("Failed to put device after verifying: %v", err) } if mach.CrossSigningKeys != nil { if device.UserID == mach.Client.UserID { err := mach.SignOwnDevice(device) if err != nil { mach.Log.Error("Failed to cross-sign own device %s: %v", device.DeviceID, err) } else { mach.Log.Debug("Cross-signed own device %v after SAS verification", device.DeviceID) } } else { masterKey, err := mach.fetchMasterKey(device, content, verState, transactionID) if err != nil { mach.Log.Warn("Failed to fetch %s's master key: %v", device.UserID, err) } else { if err := mach.SignUser(device.UserID, masterKey); err != nil { mach.Log.Error("Failed to cross-sign master key of %s: %v", device.UserID, err) } else { mach.Log.Debug("Cross-signed master key of %v after SAS verification", device.UserID) } } } } else { // TODO ask user to unlock cross-signing keys? mach.Log.Debug("Cross-signing keys not cached, not signing %s/%s", device.UserID, device.DeviceID) } mach.Log.Debug("Device %v of user %v verified successfully!", device.DeviceID, device.UserID) verState.hooks.OnSuccess() }() } // handleVerificationCancel handles an incoming m.key.verification.cancel message. // It cancels the verification process for the given reason. func (mach *OlmMachine) handleVerificationCancel(userID id.UserID, content *event.VerificationCancelEventContent, transactionID string) { // make sure to not reply with a cancel to not cause a loop of cancel messages // this verification will get canceled even if the senders do not match verStateInterface, ok := mach.keyVerificationTransactionState.Load(userID.String() + ":" + transactionID) if ok { go verStateInterface.(*verificationState).hooks.OnCancel(false, content.Reason, content.Code) } mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID) mach.Log.Warn("SAS verification %v was canceled by %v with reason: %v (%v)", transactionID, userID, content.Reason, content.Code) } // handleVerificationRequest handles an incoming m.key.verification.request message. func (mach *OlmMachine) handleVerificationRequest(userID id.UserID, content *event.VerificationRequestEventContent, transactionID string, inRoomID id.RoomID) { mach.Log.Debug("Received verification request from %v", content.FromDevice) otherDevice, err := mach.GetOrFetchDevice(userID, content.FromDevice) if err != nil { mach.Log.Error("Could not find device %v of user %v", content.FromDevice, userID) return } if !content.SupportsVerificationMethod(event.VerificationMethodSAS) { mach.Log.Warn("Canceling verification transaction %v as SAS is not supported", transactionID) if inRoomID == "" { _ = mach.SendSASVerificationCancel(otherDevice.UserID, otherDevice.DeviceID, transactionID, "Only SAS method is supported", event.VerificationCancelUnknownMethod) } else { _ = mach.SendInRoomSASVerificationCancel(inRoomID, otherDevice.UserID, transactionID, "Only SAS method is supported", event.VerificationCancelUnknownMethod) } return } resp, hooks := mach.AcceptVerificationFrom(transactionID, otherDevice, inRoomID) if resp == AcceptRequest { mach.Log.Debug("Accepting SAS verification %v from %v of user %v", transactionID, otherDevice.DeviceID, otherDevice.UserID) if inRoomID == "" { _, err = mach.NewSASVerificationWith(otherDevice, hooks, transactionID, mach.DefaultSASTimeout) } else { if err := mach.SendInRoomSASVerificationReady(inRoomID, transactionID); err != nil { mach.Log.Error("Error sending in-room SAS verification ready: %v", err) } if mach.Client.UserID < otherDevice.UserID { // up to us to send the start message _, err = mach.newInRoomSASVerificationWithInner(inRoomID, otherDevice, hooks, transactionID, mach.DefaultSASTimeout) } } if err != nil { mach.Log.Error("Error accepting SAS verification request: %v", err) } } else if resp == RejectRequest { mach.Log.Debug("Rejecting SAS verification %v from %v of user %v", transactionID, otherDevice.DeviceID, otherDevice.UserID) if inRoomID == "" { _ = mach.SendSASVerificationCancel(otherDevice.UserID, otherDevice.DeviceID, transactionID, "Not accepted by user", event.VerificationCancelByUser) } else { _ = mach.SendInRoomSASVerificationCancel(inRoomID, otherDevice.UserID, transactionID, "Not accepted by user", event.VerificationCancelByUser) } } else { mach.Log.Debug("Ignoring SAS verification %v from %v of user %v", transactionID, otherDevice.DeviceID, otherDevice.UserID) } } // NewSimpleSASVerificationWith starts the SAS verification process with another device with a default timeout, // a generated transaction ID and support for both emoji and decimal SAS methods. func (mach *OlmMachine) NewSimpleSASVerificationWith(device *DeviceIdentity, hooks VerificationHooks) (string, error) { return mach.NewSASVerificationWith(device, hooks, "", mach.DefaultSASTimeout) } // NewSASVerificationWith starts the SAS verification process with another device. // If the other device accepts the verification transaction, the methods in `hooks` will be used to verify the SAS match and to complete the transaction.. // If the transaction ID is empty, a new one is generated. func (mach *OlmMachine) NewSASVerificationWith(device *DeviceIdentity, hooks VerificationHooks, transactionID string, timeout time.Duration) (string, error) { if transactionID == "" { transactionID = strconv.Itoa(rand.Int()) } mach.Log.Debug("Starting new verification transaction %v with device %v of user %v", transactionID, device.DeviceID, device.UserID) verState := &verificationState{ sas: olm.NewSAS(), otherDevice: device, initiatedByUs: true, verificationStarted: false, keyReceived: false, sasMatched: make(chan bool, 1), hooks: hooks, } verState.lock.Lock() defer verState.lock.Unlock() startEvent, err := mach.SendSASVerificationStart(device.UserID, device.DeviceID, transactionID, hooks.VerificationMethods()) if err != nil { return "", err } payload, err := json.Marshal(startEvent) if err != nil { return "", err } canonical, err := canonicaljson.CanonicalJSON(payload) if err != nil { return "", err } verState.startEventCanonical = string(canonical) _, loaded := mach.keyVerificationTransactionState.LoadOrStore(device.UserID.String()+":"+transactionID, verState) if loaded { return "", ErrTransactionAlreadyExists } mach.timeoutAfter(verState, transactionID, timeout) return transactionID, nil } // CancelSASVerification is used by the user to cancel a SAS verification process with the given reason. func (mach *OlmMachine) CancelSASVerification(userID id.UserID, transactionID, reason string) error { mapKey := userID.String() + ":" + transactionID verStateInterface, ok := mach.keyVerificationTransactionState.Load(mapKey) if !ok { return ErrUnknownTransaction } verState := verStateInterface.(*verificationState) verState.lock.Lock() defer verState.lock.Unlock() mach.Log.Trace("User canceled verification transaction %v with reason: %v", transactionID, reason) mach.keyVerificationTransactionState.Delete(mapKey) return mach.callbackAndCancelSASVerification(verState, transactionID, reason, event.VerificationCancelByUser) } // SendSASVerificationCancel is used to manually send a SAS cancel message process with the given reason and cancellation code. func (mach *OlmMachine) SendSASVerificationCancel(userID id.UserID, deviceID id.DeviceID, transactionID string, reason string, code event.VerificationCancelCode) error { content := &event.VerificationCancelEventContent{ TransactionID: transactionID, Reason: reason, Code: code, } return mach.sendToOneDevice(userID, deviceID, event.ToDeviceVerificationCancel, content) } // SendSASVerificationStart is used to manually send the SAS verification start message to another device. func (mach *OlmMachine) SendSASVerificationStart(toUserID id.UserID, toDeviceID id.DeviceID, transactionID string, methods []VerificationMethod) (*event.VerificationStartEventContent, error) { sasMethods := make([]event.SASMethod, len(methods)) for i, method := range methods { sasMethods[i] = method.Type() } content := &event.VerificationStartEventContent{ FromDevice: mach.Client.DeviceID, TransactionID: transactionID, Method: event.VerificationMethodSAS, KeyAgreementProtocols: []event.KeyAgreementProtocol{event.KeyAgreementCurve25519HKDFSHA256}, Hashes: []event.VerificationHashMethod{event.VerificationHashSHA256}, MessageAuthenticationCodes: []event.MACMethod{event.HKDFHMACSHA256}, ShortAuthenticationString: sasMethods, } return content, mach.sendToOneDevice(toUserID, toDeviceID, event.ToDeviceVerificationStart, content) } // SendSASVerificationAccept is used to manually send an accept for a SAS verification process from a received m.key.verification.start event. func (mach *OlmMachine) SendSASVerificationAccept(fromUser id.UserID, startEvent *event.VerificationStartEventContent, publicKey []byte, methods []VerificationMethod) error { if startEvent.Method != event.VerificationMethodSAS { reason := "Unknown verification method: " + string(startEvent.Method) if err := mach.SendSASVerificationCancel(fromUser, startEvent.FromDevice, startEvent.TransactionID, reason, event.VerificationCancelUnknownMethod); err != nil { return err } return ErrUnknownVerificationMethod } payload, err := json.Marshal(startEvent) if err != nil { return err } canonical, err := canonicaljson.CanonicalJSON(payload) if err != nil { return err } hash := olm.NewUtility().Sha256(string(publicKey) + string(canonical)) sasMethods := make([]event.SASMethod, len(methods)) for i, method := range methods { sasMethods[i] = method.Type() } content := &event.VerificationAcceptEventContent{ TransactionID: startEvent.TransactionID, Method: event.VerificationMethodSAS, KeyAgreementProtocol: event.KeyAgreementCurve25519HKDFSHA256, Hash: event.VerificationHashSHA256, MessageAuthenticationCode: event.HKDFHMACSHA256, ShortAuthenticationString: sasMethods, Commitment: hash, } return mach.sendToOneDevice(fromUser, startEvent.FromDevice, event.ToDeviceVerificationAccept, content) } func (mach *OlmMachine) callbackAndCancelSASVerification(verState *verificationState, transactionID, reason string, code event.VerificationCancelCode) error { go verState.hooks.OnCancel(true, reason, code) return mach.SendSASVerificationCancel(verState.otherDevice.UserID, verState.otherDevice.DeviceID, transactionID, reason, code) } // SendSASVerificationKey sends the ephemeral public key for a device to the partner device. func (mach *OlmMachine) SendSASVerificationKey(userID id.UserID, deviceID id.DeviceID, transactionID string, key string) error { content := &event.VerificationKeyEventContent{ TransactionID: transactionID, Key: key, } return mach.sendToOneDevice(userID, deviceID, event.ToDeviceVerificationKey, content) } // SendSASVerificationMAC is use the MAC of a device's key to the partner device. func (mach *OlmMachine) SendSASVerificationMAC(userID id.UserID, deviceID id.DeviceID, transactionID string, sas *olm.SAS) error { keyID := id.NewKeyID(id.KeyAlgorithmEd25519, mach.Client.DeviceID.String()) signingKey := mach.account.SigningKey() keyIDsMap := map[id.KeyID]string{keyID: ""} macMap := make(map[id.KeyID]string) if mach.CrossSigningKeys != nil { masterKey := mach.CrossSigningKeys.MasterKey.PublicKey masterKeyID := id.NewKeyID(id.KeyAlgorithmEd25519, masterKey.String()) // add master key ID to key map keyIDsMap[masterKeyID] = "" masterKeyMAC, _, err := mach.getPKAndKeysMAC(sas, mach.Client.UserID, mach.Client.DeviceID, userID, deviceID, transactionID, masterKey, masterKeyID, keyIDsMap) if err != nil { mach.Log.Error("Error generating master key MAC: %v", err) } else { mach.Log.Debug("Generated master key `%v` MAC: %v", masterKey, masterKeyMAC) macMap[masterKeyID] = masterKeyMAC } } pubKeyMac, keysMac, err := mach.getPKAndKeysMAC(sas, mach.Client.UserID, mach.Client.DeviceID, userID, deviceID, transactionID, signingKey, keyID, keyIDsMap) if err != nil { return err } mach.Log.Debug("MAC of key %s is: %s", signingKey, pubKeyMac) mach.Log.Debug("MAC of key ID(s) %s is: %s", keyID, keysMac) macMap[keyID] = pubKeyMac content := &event.VerificationMacEventContent{ TransactionID: transactionID, Keys: keysMac, Mac: macMap, } return mach.sendToOneDevice(userID, deviceID, event.ToDeviceVerificationMAC, content) } func commonSASMethods(hooks VerificationHooks, otherDeviceMethods []event.SASMethod) []VerificationMethod { methods := make([]VerificationMethod, 0) for _, hookMethod := range hooks.VerificationMethods() { for _, otherMethod := range otherDeviceMethods { if hookMethod.Type() == otherMethod { methods = append(methods, hookMethod) break } } } return methods }