seekia/internal/messaging/peerChatKeys/peerChatKeys.go

512 lines
21 KiB
Go

// 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
}