// peerChatKeys provides functions to store user chat keys package peerChatKeys // We need to keep their keys in a seperate location, as opposed to reading user chat keys from the database // We do this because if a user's profile expires/is banned, the person chatting with them may want to continue talking to them. // If we prevented deleting their profile from the database, that could pose a fingerprinting risk when contacting hosts // Hosts would know that the requestor had been chatting with the profiles which were expired, because they would normally be deleted // Therefore, we store the user's chat keys in peerChatKeys, so we can delete their expired profile and still keep their chat keys // Their profile will be deleted from the database when the identity expires/is banned. // Active keys are keys that are a maximum of 1 day older than any any known peer latestChatKeysUpdateTime // TODO: create updateStoredChatKeys to refresh all chat keys for all saved identities // TODO: Delete chat keys for users who are not chatting partners import "seekia/internal/encoding" import "seekia/internal/helpers" import "seekia/internal/identity" import "seekia/internal/messaging/peerDevices" import "seekia/internal/myDatastores/myMap" import "seekia/internal/myDatastores/myMapList" import "seekia/internal/profiles/profileStorage" import "seekia/internal/profiles/readProfiles" import "seekia/internal/unixTime" import "strings" import "sync" import "errors" // This mutex will be locked whenever we are updating the peer chat keys MyMapList var updatingPeerChatKeysMutex sync.Mutex // This mutex will be locked whenever we are updating the peer chat keys latest update time MyMap var updatingPeerChatKeysLatestUpdateTimeMutex sync.Mutex var peerChatKeysMapListDatastore *myMapList.MyMapList // Map Structure: Peer Identity Hash + "@" + networkType -> Latest update unix time var peerChatKeysLatestUpdateTimeMapDatastore *myMap.MyMap // This function must be called whenever an app user signs in func InitializePeerChatKeysDatastores()error{ updatingPeerChatKeysMutex.Lock() defer updatingPeerChatKeysMutex.Unlock() updatingPeerChatKeysLatestUpdateTimeMutex.Lock() defer updatingPeerChatKeysLatestUpdateTimeMutex.Unlock() newPeerChatKeysMapListDatastore, err := myMapList.CreateNewMapList("PeerChatKeys") if (err != nil) { return err } newPeerChatKeysLatestUpdateTimeMapDatastore, err := myMap.CreateNewMap("PeerChatKeysLatestUpdateTime") if (err != nil) { return err } peerChatKeysMapListDatastore = newPeerChatKeysMapListDatastore peerChatKeysLatestUpdateTimeMapDatastore = newPeerChatKeysLatestUpdateTimeMapDatastore return nil } // This function will return true if we need to download a user's newest chat keys // It determines this by comparing a user's saved chat keys to their latest chat keys update time //Outputs: // -bool: User's chat keys are missing (will return false if their newest profile is disabled) // -error func CheckIfUsersChatKeysAreMissing(userIdentityHash [16]byte, networkType byte)(bool, error){ isValid := helpers.VerifyNetworkType(networkType) if (isValid == false){ networkTypeString := helpers.ConvertByteToString(networkType) return false, errors.New("CheckIfUsersChatKeysAreMissing called with invalid networkType: " + networkTypeString) } profileIsDisabled, chatKeysExist, _, _, err := GetPeerNewestActiveChatKeys(userIdentityHash, networkType) if (err != nil) { return false, err } if (profileIsDisabled == true){ // We treat this as the chat keys not being missing return false, nil } if (chatKeysExist == true){ return false, nil } return true, nil } // This function saves a user's chat key latest update time from a message // It is used to keep track of when peer updates their key set, so the app knows to retrieve new chat keys // This information could be wrong, if newer keys have been shared since the user sent their message func SavePeerMessageLatestChatKeysUpdateTime(peerIdentityHash [16]byte, networkType byte, theirLatestChatKeysUpdateTime int64, theirMessageCreationTime int64)error{ updatingPeerChatKeysLatestUpdateTimeMutex.Lock() defer updatingPeerChatKeysLatestUpdateTimeMutex.Unlock() peerIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(peerIdentityHash) if (err != nil){ peerIdentityHashHex := encoding.EncodeBytesToHexString(peerIdentityHash[:]) return errors.New("SavePeerMessageLatestChatKeysUpdateTime called with invalid peerIdentityHash: " + peerIdentityHashHex) } isValid := helpers.VerifyNetworkType(networkType) if (isValid == false){ networkTypeString := helpers.ConvertByteToString(networkType) return errors.New("SavePeerMessageLatestChatKeysUpdateTime called with invalid networkType: " + networkTypeString) } existingLatestUpdateTimeExists, existingLatestUpdateTime, existingLatestUpdateTimeCreationTime, err := getSavedPeerMessageLatestChatKeysUpdateTime(peerIdentityHash, networkType) if (err != nil){ return err } if (existingLatestUpdateTimeExists == true){ if (existingLatestUpdateTimeCreationTime > theirMessageCreationTime){ // We already have a latest update time which was sent at later time // Nothing to do return nil } // We have a latest update time that was created earler // We see if the newer created latest update time is actually newer if (existingLatestUpdateTime > theirLatestChatKeysUpdateTime){ // They are sending a latestUpdateTime that is older than one they have previously sent // The user has probably moved to a different device, and then moved back to their previous device // This should not happen // To avoid this happening we should make sure there is an option in the GUI to reinitialize the application as if it is a new device // This will require sharing a new LatestChatKeysUpdateTime in our profile // // We will delete our stored chatKeys and update our datastore latestChatKeysUpdateTime // This will allow their older chat keys to be used if and when they created a profile from their device. networkTypeString := helpers.ConvertByteToString(networkType) mapToDelete := map[string]string{ "PeerIdentityHash": peerIdentityHashString, "NetworkType": networkTypeString, } err := peerChatKeysMapListDatastore.DeleteMapListItems(mapToDelete) if (err != nil) { return err } } } theirLatestChatKeysUpdateTimeString := helpers.ConvertInt64ToString(theirLatestChatKeysUpdateTime) theirMessageCreationTimeString := helpers.ConvertInt64ToString(theirMessageCreationTime) networkTypeString := helpers.ConvertByteToString(networkType) newMapKey := peerIdentityHashString + "@" + networkTypeString newMapValue := theirLatestChatKeysUpdateTimeString + "$" + theirMessageCreationTimeString err = peerChatKeysLatestUpdateTimeMapDatastore.SetMapEntry(newMapKey, newMapValue) if (err != nil) { return err } return nil } // This function retrieves our saved peer latest chat keys update time that we received in a message //Outputs: // -bool: Latest update time info exists // -int64: Latest Update Time // -int64: Latest update time creation time (the time when the user shared this latestUpdateTime) // -error func getSavedPeerMessageLatestChatKeysUpdateTime(peerIdentityHash [16]byte, networkType byte)(bool, int64, int64, error){ peerIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(peerIdentityHash) if (err != nil){ peerIdentityHashHex := encoding.EncodeBytesToHexString(peerIdentityHash[:]) return false, 0, 0, errors.New("getSavedPeerMessageLatestChatKeysUpdateTime called with invalid peerIdentityHash: " + peerIdentityHashHex) } isValid := helpers.VerifyNetworkType(networkType) if (isValid == false){ networkTypeString := helpers.ConvertByteToString(networkType) return false, 0, 0, errors.New("getSavedPeerMessageLatestChatKeysUpdateTime called with invalid networkType: " + networkTypeString) } networkTypeString := helpers.ConvertByteToString(networkType) mapKey := peerIdentityHashString + "@" + networkTypeString entryExists, mapValue, err := peerChatKeysLatestUpdateTimeMapDatastore.GetMapEntry(mapKey) if (err != nil) { return false, 0, 0, err } if (entryExists == false){ return false, 0, 0, nil } latestUpdateTime, latestUpdateTimeCreationTime, delimiterFound := strings.Cut(mapValue, "$") if (delimiterFound == false){ return false, 0, 0, errors.New("peerChatKeysLatestUpdateTimeMapDatastore entry is malformed: Map value missing $: " + mapValue) } latestUpdateTimeInt64, err := helpers.ConvertStringToInt64(latestUpdateTime) if (err != nil){ return false, 0, 0, errors.New("peerChatKeysLatestUpdateTimeMapDatastore entry value is malformed: Invalid latestUpdateTime: " + latestUpdateTime) } latestUpdateTimeCreationTimeInt64, err := helpers.ConvertStringToInt64(latestUpdateTimeCreationTime) if (err != nil){ return false, 0, 0, errors.New("peerChatKeysLatestUpdateTimeMapDatastore entry value is malformed: Invalid latestUpdateTimeCreationTime: " + latestUpdateTimeCreationTime) } return true, latestUpdateTimeInt64, latestUpdateTimeCreationTimeInt64, nil } // This function returns the newest saved device chat keys list for a user // It also checks saved profiles to update saved keys. //Outputs: // -bool: Peer newest profile is known to be disabled // -bool: Peer active chat keys found // -[32]byte: Nacl Key // -[1568]byte: Kyber key // -error func GetPeerNewestActiveChatKeys(peerIdentityHash [16]byte, networkType byte)(bool, bool, [32]byte, [1568]byte, error){ peerIdentityHashString, userIdentityType, err := identity.EncodeIdentityHashBytesToString(peerIdentityHash) if (err != nil) { peerIdentityHashHex := encoding.EncodeBytesToHexString(peerIdentityHash[:]) return false, false, [32]byte{}, [1568]byte{}, errors.New("GetPeerNewestActiveChatKeys called with invalid peerIdentityHash: " + peerIdentityHashHex) } if (userIdentityType != "Mate" && userIdentityType != "Moderator"){ return false, false, [32]byte{}, [1568]byte{}, errors.New("Trying to get peer chat keys for host identity") } isValid := helpers.VerifyNetworkType(networkType) if (isValid == false){ networkTypeString := helpers.ConvertByteToString(networkType) return false, false, [32]byte{}, [1568]byte{}, errors.New("GetPeerNewestActiveChatKeys called with invalid networkType: " + networkTypeString) } updatingPeerChatKeysMutex.Lock() defer updatingPeerChatKeysMutex.Unlock() //Outputs: // -bool: Peer newest profile is known to be disabled // -bool: Any keys found // -[32]byte: User Nacl key // -[1568]byte: User Kyber key // -int64: Time keys were created // -error getPeerNewestKnownChatKeys := func()(bool, bool, [32]byte, [1568]byte, int64, error){ // We get newest known user chat keys // We check database and storage, and return whichever are newer // Database = Profiles within the badger database // Storage = myMapList datastore //Outputs: // -bool: Profile found // -bool: Profile is disabled // -int64: Profile creation time // -[32]byte: Peer Nacl key // -[1568]byte: Peer Kyber key // -error getPeerChatKeysMapFromDatabase := func()(bool, bool, int64, [32]byte, [1568]byte, error){ profileExists, _, _, _, profileCreationTime, rawProfileMap, err := profileStorage.GetNewestUserProfile(peerIdentityHash, networkType) if (err != nil) { return false, false, 0, [32]byte{}, [1568]byte{}, err } if (profileExists == false) { return false, false, 0, [32]byte{}, [1568]byte{}, nil } profileIsDisabled, _, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(rawProfileMap, "Disabled") if (err != nil) { return false, false, 0, [32]byte{}, [1568]byte{}, err } if (profileIsDisabled == true){ return true, true, profileCreationTime, [32]byte{}, [1568]byte{}, nil } exists, peerNaclKey, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(rawProfileMap, "NaclKey") if (err != nil) { return false, false, 0, [32]byte{}, [1568]byte{}, err } if (exists == false){ return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Database corrupt: Contains Mate/Moderator profile missing NaclKey") } exists, peerKyberKey, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(rawProfileMap, "KyberKey") if (err != nil) { return false, false, 0, [32]byte{}, [1568]byte{}, err } if (exists == false){ return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Database corrupt: Contains Mate/Moderator profile missing KyberKey") } peerNaclKeyBytes, err := encoding.DecodeBase64StringToBytes(peerNaclKey) if (err != nil){ return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Database corrupt: Contains Mate/Moderator profile with invalid NaclKey: Not Base64: " + peerNaclKey) } if (len(peerNaclKeyBytes) != 32){ return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Database corrupt: Contains Mate/Moderator profile with invalid NaclKey: Invalid length: " + peerNaclKey) } peerNaclKeyArray := [32]byte(peerNaclKeyBytes) peerKyberKeyBytes, err := encoding.DecodeBase64StringToBytes(peerKyberKey) if (err != nil){ return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Database corrupt: Contains Mate/Moderator profile with invalid KyberKey: Not Base64: " + peerKyberKey) } if (len(peerKyberKeyBytes) != 1568){ return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Database corrupt: Contains Mate/Moderator profile with invalid KyberKey: Invalid length: " + peerKyberKey) } peerKyberKeyArray := [1568]byte(peerKyberKeyBytes) return true, false, profileCreationTime, peerNaclKeyArray, peerKyberKeyArray, nil } //Outputs: // -bool: Peer stored keys found // -bool: Peer is disabled // -int64: Creation time of these keys/disabled status // -[32]byte: Peer Nacl Key // -[1568]byte: Peer Kyber key // -error getPeerChatKeysMapListFromMyMapListStorage := func()(bool, bool, int64, [32]byte, [1568]byte, error){ networkTypeString := helpers.ConvertByteToString(networkType) lookupMap := map[string]string{ "PeerIdentityHash": peerIdentityHashString, "NetworkType": networkTypeString, } exists, retrievedMapListItems, err := peerChatKeysMapListDatastore.GetMapListItems(lookupMap) if (err != nil) { return false, false, 0, [32]byte{}, [1568]byte{}, err } if (exists == false){ return false, false, 0, [32]byte{}, [1568]byte{}, nil } if (len(retrievedMapListItems) != 1) { return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Malformed peer chat keys maplist: multiple entries found.") } peerChatKeysMap := retrievedMapListItems[0] keysCreationTime, exists := peerChatKeysMap["CreationTime"] if (exists == false) { return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Malformed peer chat keys maplist: Item missing CreationTime") } keysCreationTimeInt64, err := helpers.ConvertCreationTimeStringToInt64(keysCreationTime) if (err != nil) { return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Malformed peer chat keys maplist: Item contains invalid CreationTime: " + keysCreationTime) } peerIsDisabled, exists := peerChatKeysMap["PeerIsDisabled"] if (exists == true && peerIsDisabled == "Yes"){ return true, true, keysCreationTimeInt64, [32]byte{}, [1568]byte{}, nil } peerNaclKey, exists := peerChatKeysMap["NaclKey"] if (exists == false) { return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Malformed peer chat keys maplist: Item missing NaclKey") } peerKyberKey, exists := peerChatKeysMap["KyberKey"] if (exists == false) { return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Malformed peer chat keys maplist: Item missing KyberKey") } peerNaclKeyBytes, err := encoding.DecodeBase64StringToBytes(peerNaclKey) if (err != nil){ return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Malformed peer chat keys maplist: Item contains invalid NaclKey: Not Base64: " + peerNaclKey) } if (len(peerNaclKeyBytes) != 32){ return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Malformed peer chat keys maplist: Item contains invalid NaclKey: Invalid length: " + peerNaclKey) } peerNaclKeyArray := [32]byte(peerNaclKeyBytes) peerKyberKeyBytes, err := encoding.DecodeBase64StringToBytes(peerKyberKey) if (err != nil){ return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Malformed peer chat keys maplist: Item contains invalid KyberKey: Not Base64: " + peerKyberKey) } if (len(peerKyberKeyBytes) != 1568){ return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Malformed peer chat keys maplist: Item contains invalid KyberKey: Invalid length: " + peerKyberKey) } peerKyberKeyArray := [1568]byte(peerKyberKeyBytes) return true, false, keysCreationTimeInt64, peerNaclKeyArray, peerKyberKeyArray, nil } databaseUserProfileExists, databaseProfileIsDisabled, databaseUserProfileCreationTime, databaseUserNaclKey, databaseUserKyberKey, err := getPeerChatKeysMapFromDatabase() if (err != nil) { return false, false, [32]byte{}, [1568]byte{}, 0, err } storageUserChatKeysInfoExists, storageProfileIsDisabled, storageUserProfileCreationTime, storageUserNaclKey, storageUserKyberKey, err := getPeerChatKeysMapListFromMyMapListStorage() if (err != nil) { return false, false, [32]byte{}, [1568]byte{}, 0, err } if (databaseUserProfileExists == false){ if (storageUserChatKeysInfoExists == false){ return false, false, [32]byte{}, [1568]byte{}, 0, nil } if (storageProfileIsDisabled == true){ return true, false, [32]byte{}, [1568]byte{}, 0, nil } return false, true, storageUserNaclKey, storageUserKyberKey, storageUserProfileCreationTime, nil } // Database chat keys exist if (storageUserChatKeysInfoExists == true){ // Both the database profile and the datastore info exist // We see which contains newer information if (databaseUserProfileCreationTime <= storageUserProfileCreationTime){ // myMapList storage is up to date, or even more recent than the stored profile // The latter would occur if a newer profile was deleted at some point, and an older profile was downloaded // We return the myMapList stored data if (storageProfileIsDisabled == true){ return true, false, [32]byte{}, [1568]byte{}, 0, nil } return false, true, storageUserNaclKey, storageUserKyberKey, storageUserProfileCreationTime, nil } } // MyMapList chat keys entry is either missing or out of date. // Update them with database profile keys, and return the database profile chat keys // Note that just because the created profile is newer does not mean the chat keys are any different. networkTypeString := helpers.ConvertByteToString(networkType) deleteLookupMap := map[string]string{ "PeerIdentityHash": peerIdentityHashString, "NetworkType": networkTypeString, } err = peerChatKeysMapListDatastore.DeleteMapListItems(deleteLookupMap) if (err != nil) { return false, false, [32]byte{}, [1568]byte{}, 0, err } creationTimeString := helpers.ConvertInt64ToString(databaseUserProfileCreationTime) newChatKeysMap := map[string]string{ "PeerIdentityHash": peerIdentityHashString, "NetworkType": networkTypeString, "CreationTime": creationTimeString, } if (databaseProfileIsDisabled == true){ newChatKeysMap["PeerIsDisabled"] = "Yes" } else { databaseUserNaclKeyString := encoding.EncodeBytesToBase64String(databaseUserNaclKey[:]) databaseUserKyberKeyString := encoding.EncodeBytesToBase64String(databaseUserKyberKey[:]) newChatKeysMap["NaclKey"] = databaseUserNaclKeyString newChatKeysMap["KyberKey"] = databaseUserKyberKeyString } err = peerChatKeysMapListDatastore.AddMapListItem(newChatKeysMap) if (err != nil) { return false, false, [32]byte{}, [1568]byte{}, 0, err } if (databaseProfileIsDisabled == true){ return true, false, [32]byte{}, [1568]byte{}, 0, nil } return false, true, databaseUserNaclKey, databaseUserKyberKey, databaseUserProfileCreationTime, nil } peerProfileIsDisabled, peerChatKeysExist, peerNewestKnownNaclKey, peerNewestKnownKyberKey, peerNewestKnownKeysCreationTime, err := getPeerNewestKnownChatKeys() if (err != nil) { return false, false, [32]byte{}, [1568]byte{}, err } if (peerProfileIsDisabled == true){ return true, false, [32]byte{}, [1568]byte{}, nil } if (peerChatKeysExist == false){ return false, false, [32]byte{}, [1568]byte{}, nil } // We check to see if user has updated their device since they created these keys deviceInfoFound, _, deviceFirstSeenTime, err := peerDevices.GetPeerNewestDeviceInfo(peerIdentityHash, networkType) if (err != nil) { return false, false, [32]byte{}, [1568]byte{}, err } if (deviceInfoFound == true){ if (deviceFirstSeenTime > peerNewestKnownKeysCreationTime){ // User has changed their device since they created these keys // We do not have access to the user's chat keys return false, false, [32]byte{}, [1568]byte{}, nil } } // Now we check to see if user has sent a newer ChatKeysLatestUpdateTime // We allow a 1 day grace period where old keys will still work newestUpdateTimeIsKnown, newestUpdateTime, _, err := getSavedPeerMessageLatestChatKeysUpdateTime(peerIdentityHash, networkType) if (err != nil) { return false, false, [32]byte{}, [1568]byte{}, err } if (newestUpdateTimeIsKnown == true){ dayUnix := unixTime.GetDayUnix() keysExpirationTime := newestUpdateTime - dayUnix if (peerNewestKnownKeysCreationTime < keysExpirationTime){ return false, false, [32]byte{}, [1568]byte{}, nil } } return false, true, peerNewestKnownNaclKey, peerNewestKnownKyberKey, nil }