// mySecretInboxes provides functions to manage a user's secret inboxes // User sends these inboxes out in messages to users // They must check them for messages until they expire package mySecretInboxes //TODO: Prune expired inboxes we have checked sufficiently import "seekia/internal/encoding" import "seekia/internal/helpers" import "seekia/internal/identity" import "seekia/internal/messaging/inbox" import "seekia/internal/messaging/secretInboxEpoch" import "seekia/internal/myBlockedUsers" import "seekia/internal/myDatastores/myMapList" import "bytes" import "sync" import "errors" // This will be locked whenever we update the map list var updatingMySecretInboxesMapListMutex sync.Mutex var mySecretInboxesMapListDatastore *myMapList.MyMapList // This function must be called whenever an app user signs in func InitializeMySecretInboxesDatastore()error{ updatingMySecretInboxesMapListMutex.Lock() defer updatingMySecretInboxesMapListMutex.Unlock() newMySecretInboxesMapListDatastore, err := myMapList.CreateNewMapList("MySecretInboxes") if (err != nil) { return err } mySecretInboxesMapListDatastore = newMySecretInboxesMapListDatastore return nil } // This function will omit inboxes that we no longer need to download messages for // This would be because they have been sufficiently checked and old enough that no new messages are being received func GetAllMyActiveSecretInboxes(myIdentityHash [16]byte, networkType byte)([][10]byte, error){ isValid, err := identity.VerifyIdentityHash(myIdentityHash, false, "") if (err != nil) { return nil, err } if (isValid == false){ myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) return nil, errors.New("GetAllMyActiveSecretInboxes called with invalid myIdentityHash: " + myIdentityHashHex) } isValid = helpers.VerifyNetworkType(networkType) if (isValid == false){ networkTypeString := helpers.ConvertByteToString(networkType) return nil, errors.New("GetAllMyActiveSecretInboxes called with invalid networkType: " + networkTypeString) } mySecretInboxesList, err := GetAllMySecretInboxes(myIdentityHash, networkType) if (err != nil){ return nil, err } //TODO: We need code that keeps track of how many times we have checked a particular inbox after it has expired // Then we will omit those inboxes return mySecretInboxesList, nil } // This function will not return inboxes for users whom the user has blocked func GetAllMySecretInboxes(myIdentityHash [16]byte, networkType byte)([][10]byte, error){ isValid, err := identity.VerifyIdentityHash(myIdentityHash, false, "") if (err != nil) { return nil, err } if (isValid == false){ myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) return nil, errors.New("GetAllMySecretInboxes called with invalid myIdentityHash: " + myIdentityHashHex) } isValid = helpers.VerifyNetworkType(networkType) if (isValid == false){ networkTypeString := helpers.ConvertByteToString(networkType) return nil, errors.New("GetAllMySecretInboxes called with invalid networkType: " + networkTypeString) } mySecretInboxesMapList, err := mySecretInboxesMapListDatastore.GetMapList() if (err != nil) { return nil, err } allMySecretInboxesList := make([][10]byte, 0) for _, inboxMap := range mySecretInboxesMapList{ conversationMyIdentityHashString, exists := inboxMap["MyIdentityHash"] if (exists == false){ return nil, errors.New("mySecretInboxesMapList is malformed: Item missing MyIdentityHash.") } conversationMyIdentityHash, _, err := identity.ReadIdentityHashString(conversationMyIdentityHashString) if (err != nil){ return nil, errors.New("mySecretInboxesMapList is malformed: Item contains invalid MyIdentityHash: " + conversationMyIdentityHashString) } if (conversationMyIdentityHash != myIdentityHash){ continue } recipientIdentityHashString, exists := inboxMap["TheirIdentityHash"] if (exists == false){ return nil, errors.New("mySecretInboxesMapList is malformed: Item missing TheirIdentityHash") } recipientIdentityHash, _, err := identity.ReadIdentityHashString(recipientIdentityHashString) if (err != nil){ return nil, errors.New("mySecretInboxesMapList is malformed: Item contains invalid TheirIdentityHash: " + recipientIdentityHashString) } recipientIsBlocked, _, _, _, err := myBlockedUsers.CheckIfUserIsBlocked(recipientIdentityHash) if (err != nil){ return nil, err } if (recipientIsBlocked == true){ continue } inboxNetworkTypeString, exists := inboxMap["NetworkType"] if (exists == false){ return nil, errors.New("mySecretInboxesMapList is malformed: Item missing NetworkType.") } inboxNetworkType, err := helpers.ConvertNetworkTypeStringToByte(inboxNetworkTypeString) if (err != nil){ return nil, errors.New("mySecretInboxesMapList is malformed: Item contains invalid NetworkType: " + inboxNetworkTypeString) } if (inboxNetworkType != networkType){ continue } secretInboxSeedHex, exists := inboxMap["SecretInboxSeed"] if (exists == false){ return nil, errors.New("mySecretInboxesMapList is malformed: Item missing SecretInboxSeed.") } secretInboxSeed, err := encoding.DecodeHexStringToBytes(secretInboxSeedHex) if (err != nil){ return nil, errors.New("mySecretInboxesMapList is malformed: Contains invalid secretInboxSeed: Not Hex.") } if (len(secretInboxSeed) != 22){ return nil, errors.New("mySecretInboxesMapList is malformed: Contains invalid secretInboxSeed: Invalid length.") } secretInboxSeedArray := [22]byte(secretInboxSeed) currentSecretInbox, _, err := inbox.GetSecretInboxAndSealerKeyFromSecretInboxSeed(secretInboxSeedArray) if (err != nil){ return nil, err } allMySecretInboxesList = append(allMySecretInboxesList, currentSecretInbox) } return allMySecretInboxesList, nil } //Outputs: // -bool: My secret inbox found // -[16]byte: Conversation my identity hash // -[16]byte: Conversation recipient identity hash // -byte: Secret inbox network type // -[32]byte: Secret inbox sealer key // -int64: Secret inbox start time // -int64: Secret inbox expiration time // -error func GetMySecretInboxInfo(secretInbox [10]byte)(bool, [16]byte, [16]byte, byte, [32]byte, int64, int64, error){ mySecretInboxesMapList, err := mySecretInboxesMapListDatastore.GetMapList() if (err != nil) { return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, err } for _, inboxMap := range mySecretInboxesMapList{ secretInboxSeedHex, exists := inboxMap["SecretInboxSeed"] if (exists == false){ return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxesMapList is malformed: Inbox map missing SecretInboxSeed") } secretInboxSeed, err := encoding.DecodeHexStringToBytes(secretInboxSeedHex) if (err != nil){ return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxes map list malformed: Contains invalid secretInboxSeed: Not Hex.") } if (len(secretInboxSeed) != 22){ return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxes map list malformed: Contains invalid secretInboxSeed: Invalid length.") } secretInboxSeedArray := [22]byte(secretInboxSeed) currentSecretInbox, currentSecretInboxSealerKey, err := inbox.GetSecretInboxAndSealerKeyFromSecretInboxSeed(secretInboxSeedArray) if (err != nil){ return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxes map list malformed: Contains invalid secretInboxSeed.") } if (currentSecretInbox != secretInbox){ continue } conversationMyIdentityHashString, exists := inboxMap["MyIdentityHash"] if (exists == false){ return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxesMapList is malformed: Inbox map missing MyIdentityHash") } conversationMyIdentityHash, _, err := identity.ReadIdentityHashString(conversationMyIdentityHashString) if (err != nil){ return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxesMapList is malformed: Inbox map contains invalid MyIdentityHash: " + conversationMyIdentityHashString) } conversationTheirIdentityHashString, exists := inboxMap["TheirIdentityHash"] if (exists == false){ return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxesMapList is malformed: Inbox map missing TheirIdentityHash") } conversationTheirIdentityHash, _, err := identity.ReadIdentityHashString(conversationTheirIdentityHashString) if (err != nil){ return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxesMapList is malformed: Inbox map contains invalid TheirIdentityHash: " + conversationTheirIdentityHashString) } inboxNetworkTypeString, exists := inboxMap["NetworkType"] if (exists == false){ return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxesMapList is malformed: Inbox map missing NetworkType") } inboxNetworkType, err := helpers.ConvertNetworkTypeStringToByte(inboxNetworkTypeString) if (err != nil){ return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxesMapList is malformed: Inbox map contains invalid NetworkType: " + inboxNetworkTypeString) } inboxStartTimeString, exists := inboxMap["InboxStartTime"] if (exists == false){ return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxesMapList is malformed: Inbox map missing InboxStartTime") } inboxStartTimeInt64, err := helpers.ConvertStringToInt64(inboxStartTimeString) if (err != nil) { return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("Malformed mySecretInboxesMapList: Invalid secret inbox start time: " + inboxStartTimeString) } inboxEndTimeString, exists := inboxMap["InboxEndTime"] if (exists == false){ return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxesMapList is malformed: Inbox map missing InboxEndTime") } inboxEndTimeInt64, err := helpers.ConvertStringToInt64(inboxEndTimeString) if (err != nil) { return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("Malformed mySecretInboxesMapList: Invalid secret inbox end time: " + inboxEndTimeString) } return true, conversationMyIdentityHash, conversationTheirIdentityHash, inboxNetworkType, currentSecretInboxSealerKey, inboxStartTimeInt64, inboxEndTimeInt64, nil } return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, nil } //Outputs: // -bool: Parameters exist // -bool: My current epoch secret inbox seed exists // -[22]byte: My current epoch secret inbox seed // -bool: My next epoch secret inbox seed exists // -[22]byte: My next epoch secret inbox seed // -error func GetMySecretInboxSeedsForMessage(myIdentityHash [16]byte, recipientIdentityHash [16]byte, networkType byte, messageCreationTimeUnix int64)(bool, bool, [22]byte, bool, [22]byte, error){ isValid, err := identity.VerifyIdentityHash(myIdentityHash, false, "") if (err != nil){ return false, false, [22]byte{}, false, [22]byte{}, err } if (isValid == false){ myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) return false, false, [22]byte{}, false, [22]byte{}, errors.New("GetMySecretInboxSeedsForMessage called with invalid myIdentityHash: " + myIdentityHashHex) } isValid = helpers.VerifyNetworkType(networkType) if (isValid == false){ networkTypeString := helpers.ConvertByteToString(networkType) return false, false, [22]byte{}, false, [22]byte{}, errors.New("GetMySecretInboxSeedsForMessage called with invalid networkType: " + networkTypeString) } parametersExist, currentEpochStartTime, currentEpochEndTime, nextEpochStartTime, nextEpochEndTime, err := secretInboxEpoch.GetSecretInboxEpochStartAndEndTimes(networkType, messageCreationTimeUnix) if (err != nil) { return false, false, [22]byte{}, false, [22]byte{}, err } if (parametersExist == false){ return false, false, [22]byte{}, false, [22]byte{}, nil } myIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) if (err != nil){ myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) return false, false, [22]byte{}, false, [22]byte{}, errors.New("VerifyIdentityHash failed to verify identity hash: " + myIdentityHashHex) } recipientIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(recipientIdentityHash) if (err != nil){ recipientIdentityHashHex := encoding.EncodeBytesToHexString(recipientIdentityHash[:]) return false, false, [22]byte{}, false, [22]byte{}, errors.New("GetMySecretInboxSeedsForMessage called with invalid recipient identity hash: " + recipientIdentityHashHex) } // We retrieve all secret inboxes for this conversation, and find an inbox that exists for the entire desired epoch duration networkTypeString := helpers.ConvertByteToString(networkType) lookupMap := map[string]string{ "MyIdentityHash": myIdentityHashString, "TheirIdentityHash": recipientIdentityHashString, "NetworkType": networkTypeString, } anyItemsFound, foundItemsMapList, err := mySecretInboxesMapListDatastore.GetMapListItems(lookupMap) if (err != nil) { return false, false, [22]byte{}, false, [22]byte{}, err } if (anyItemsFound == false){ return true, false, [22]byte{}, false, [22]byte{}, nil } //Outputs: // -bool: Inbox found // -[22]byte: Secret inbox seed // -error getSecretInboxSeedForEpoch := func(epochStartTime int64, epochEndTime int64)(bool, [22]byte, error){ for _, inboxMap := range foundItemsMapList{ inboxStartTimeString, exists := inboxMap["InboxStartTime"] if (exists == false){ return false, [22]byte{}, errors.New("MySecretInboxesMapList is malformed: Item missing InboxStartTime") } inboxStartTimeInt64, err := helpers.ConvertStringToInt64(inboxStartTimeString) if (err != nil) { return false, [22]byte{}, errors.New("MySecretInboxesMapList is malformed: Invalid secret inbox start time: " + inboxStartTimeString) } inboxEndTimeString, exists := inboxMap["InboxEndTime"] if (exists == false){ return false, [22]byte{}, errors.New("MySecretInboxesMapList is malformed: Item missing InboxEndTime") } inboxEndTimeInt64, err := helpers.ConvertStringToInt64(inboxEndTimeString) if (err != nil) { return false, [22]byte{}, errors.New("MySecretInboxesMapList is malformed: Invalid secret inbox end time: " + inboxEndTimeString) } if (inboxStartTimeInt64 > epochStartTime || inboxEndTimeInt64 < epochEndTime){ continue } secretInboxSeedHex, exists := inboxMap["SecretInboxSeed"] if (exists == false){ return false, [22]byte{}, errors.New("MySecretInboxesMapList is malformed: Item missing SecretInboxSeed") } secretInboxSeed, err := encoding.DecodeHexStringToBytes(secretInboxSeedHex) if (err != nil){ return false, [22]byte{}, errors.New("MySecretInboxesMapList is malformed: Invalid secret inbox seed: " + secretInboxSeedHex) } if (len(secretInboxSeed) != 22){ return false, [22]byte{}, errors.New("MySecretInboxesMapList is malformed: Invalid secret inbox seed: " + secretInboxSeedHex) } secretInboxSeedArray := [22]byte(secretInboxSeed) return true, secretInboxSeedArray, nil } return false, [22]byte{}, nil } foundCurrentEpochInboxSeed, currentEpochInboxSeed, err := getSecretInboxSeedForEpoch(currentEpochStartTime, currentEpochEndTime) if (err != nil) { return false, false, [22]byte{}, false, [22]byte{}, err } foundNextEpochInboxSeed, nextEpochInboxSeed, err := getSecretInboxSeedForEpoch(nextEpochStartTime, nextEpochEndTime) if (err != nil) { return false, false, [22]byte{}, false, [22]byte{}, err } return true, foundCurrentEpochInboxSeed, currentEpochInboxSeed, foundNextEpochInboxSeed, nextEpochInboxSeed, nil } //Outputs: // -bool: Parameters exist // -error func AddMySecretInboxSeeds(myIdentityHash [16]byte, theirIdentityHash [16]byte, networkType byte, messageCreationTime int64, currentSecretInboxSeed [22]byte, nextSecretInboxSeed [22]byte)(bool, error){ isValid, err := identity.VerifyIdentityHash(myIdentityHash, false, "") if (err != nil) { return false, err } if (isValid == false){ myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) return false, errors.New("AddMySecretInboxSeeds called with invalid myIdentityHash: " + myIdentityHashHex) } isValid, err = identity.VerifyIdentityHash(theirIdentityHash, false, "") if (err != nil) { return false, err } if (isValid == false){ theirIdentityHashHex := encoding.EncodeBytesToHexString(theirIdentityHash[:]) return false, errors.New("AddMySecretInboxSeeds called with invalid theirIdentityHash: " + theirIdentityHashHex) } isValid = helpers.VerifyNetworkType(networkType) if (isValid == false){ networkTypeString := helpers.ConvertByteToString(networkType) return false, errors.New("AddMySecretInboxSeeds called with invalid networkType: " + networkTypeString) } updatingMySecretInboxesMapListMutex.Lock() defer updatingMySecretInboxesMapListMutex.Unlock() parametersExist, currentSecretInboxStartTime, currentSecretInboxEndTime, nextSecretInboxStartTime, nextSecretInboxEndTime, err := secretInboxEpoch.GetSecretInboxEpochStartAndEndTimes(networkType, messageCreationTime) if (err != nil) { return false, err } if (parametersExist == false){ return false, nil } mySecretInboxesMapList, err := mySecretInboxesMapListDatastore.GetMapList() if (err != nil) { return false, err } addSecretInboxSeedToMapList := func(secretInboxSeed [22]byte, inboxStartTime int64, inboxEndTime int64)error{ // We see if an entry already exists for this inbox for _, inboxMap := range mySecretInboxesMapList{ currentSecretInboxSeedString, exists := inboxMap["SecretInboxSeed"] if (exists == false){ return errors.New("mySecretInboxesMapListDatastore is malformed: Item missing SecretInboxSeed") } currentSecretInboxSeedBytes, err := encoding.DecodeHexStringToBytes(currentSecretInboxSeedString) if (err != nil){ return errors.New("mySecretInboxesMapListDatastore is malformed: Item contains invalid SecretInboxSeed: Not Hex.") } if (len(currentSecretInboxSeedBytes) != 22){ return errors.New("mySecretInboxesMapListDatastore is malformed: Item contains invalid SecretInboxSeed: Invalid length.") } areEqual := bytes.Equal(currentSecretInboxSeedBytes, secretInboxSeed[:]) if (areEqual == false){ continue } conversationMyIdentityHashString, exists := inboxMap["MyIdentityHash"] if (exists == false){ return errors.New("Malformed mySecretInboxesMapListDatastore: Item missing MyIdentityHash") } conversationMyIdentityHash, _, err := identity.ReadIdentityHashString(conversationMyIdentityHashString) if (err != nil){ return errors.New("Malformed mySecretInboxesMapListDatastore: Item contains invalid MyIdentityHash: " + conversationMyIdentityHashString) } if (conversationMyIdentityHash != myIdentityHash){ // This should never happen. Secret inbox seeds are generated randomly. return errors.New("Trying to add a secret inbox seed which already exists for a different identity.") } conversationTheirIdentityHashString, exists := inboxMap["TheirIdentityHash"] if (exists == false) { return errors.New("Malformed mySecretInboxesMapListDatastore: Item missing TheirIdentityHash") } conversationTheirIdentityHash, _, err := identity.ReadIdentityHashString(conversationTheirIdentityHashString) if (err != nil){ return errors.New("Malformed mySecretInboxesMapListDatastore: Item contains invalid TheirIdentityHash: " + conversationTheirIdentityHashString) } if (conversationTheirIdentityHash != theirIdentityHash){ // This should never happen. Secret inbox seeds are generated randomly. return errors.New("Trying to add a secret inbox seed which already exists for a different recipient identity.") } inboxNetworkTypeString, exists := inboxMap["NetworkType"] if (exists == false){ return errors.New("Malformed mySecretInboxesMapListDatastore: Item missing NetworkType") } inboxNetworkType, err := helpers.ConvertNetworkTypeStringToByte(inboxNetworkTypeString) if (err != nil){ return errors.New("Malformed mySecretInboxesMapListDatastore: Item contains invalid NetworkType: " + inboxNetworkTypeString) } if (inboxNetworkType != networkType){ // This should never happen. Secret inbox seeds are generated randomly. return errors.New("Trying to add a secret inbox seed that already exists with a different networkType.") } existingInboxStartTimeString, exists := inboxMap["InboxStartTime"] if (exists == false){ return errors.New("mySecretInboxesMapListDatastore is malformed: Item missing InboxStartTime") } existingInboxStartTime, err := helpers.ConvertStringToInt64(existingInboxStartTimeString) if (err != nil) { return errors.New("Malformed mySecretInboxesMapList: Invalid secret inbox start time: " + existingInboxStartTimeString) } existingInboxEndTimeString, exists := inboxMap["InboxEndTime"] if (exists == false){ return errors.New("mySecretInboxesMapListDatastore is malformed: Item missing InboxEndTime") } existingInboxEndTime, err := helpers.ConvertStringToInt64(existingInboxEndTimeString) if (err != nil) { return errors.New("Malformed mySecretInboxesMapList: Invalid secret inbox end time: " + existingInboxEndTimeString) } // We see if the existing inbox start/end times are earlier/later than what we have recorded // This would only happen if secret inbox epoch duration was changed if (inboxStartTime >= existingInboxStartTime && inboxEndTime <= existingInboxEndTime){ // Existing inbox range bounds are valid. // Nothing to change return nil } // Inbox range must be expanded // Secret inbox epoch duration must have changed (or sender is malicious) newInboxStartTime := min(existingInboxStartTime, inboxStartTime) newInboxEndTime := max(existingInboxEndTime, inboxEndTime) newSecretInboxStartTimeString := helpers.ConvertInt64ToString(newInboxStartTime) newSecretInboxEndTimeString := helpers.ConvertInt64ToString(newInboxEndTime) inboxMap["InboxStartTime"] = newSecretInboxStartTimeString inboxMap["InboxEndTime"] = newSecretInboxEndTimeString return nil } // This is only reached if inbox map does not exist // We create and append a new inbox map myIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) if (err != nil) { myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) return errors.New("VerifyIdentityHash failed to verify myIdentityHash: " + myIdentityHashHex) } theirIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(theirIdentityHash) if (err != nil) { theirIdentityHashHex := encoding.EncodeBytesToHexString(theirIdentityHash[:]) return errors.New("VerifyIdentityHash failed to verify theirIdentityHash: " + theirIdentityHashHex) } secretInboxSeedHex := encoding.EncodeBytesToHexString(secretInboxSeed[:]) networkTypeString := helpers.ConvertByteToString(networkType) secretInboxStartTimeString := helpers.ConvertInt64ToString(currentSecretInboxStartTime) secretInboxEndTimeString := helpers.ConvertInt64ToString(currentSecretInboxEndTime) newInboxMap := map[string]string{ "MyIdentityHash": myIdentityHashString, "TheirIdentityHash": theirIdentityHashString, "SecretInboxSeed": secretInboxSeedHex, "NetworkType": networkTypeString, "InboxStartTime": secretInboxStartTimeString, "InboxEndTime": secretInboxEndTimeString, } mySecretInboxesMapList = append(mySecretInboxesMapList, newInboxMap) return nil } err = addSecretInboxSeedToMapList(currentSecretInboxSeed, currentSecretInboxStartTime, currentSecretInboxEndTime) if (err != nil) { return false, err } err = addSecretInboxSeedToMapList(nextSecretInboxSeed, nextSecretInboxStartTime, nextSecretInboxEndTime) if (err != nil) { return false, err } err = mySecretInboxesMapListDatastore.OverwriteMapList(mySecretInboxesMapList) if (err != nil) { return false, err } return true, nil }