// readProfiles provides functions to read and validate user profiles package readProfiles import "seekia/internal/cryptography/blake3" import "seekia/internal/cryptography/edwardsKeys" import "seekia/internal/encoding" import "seekia/internal/helpers" import "seekia/internal/identity" import "seekia/internal/profiles/profileFormat" import messagepack "github.com/vmihailenco/msgpack/v5" import "strings" import "encoding/binary" import "errors" import "slices" // Verifies a profile //Outputs: // -bool: Is valid // -error: Will return error if there is a bug in the function func VerifyProfile(inputProfile []byte)(bool, error){ ableToRead, _, _, _, _, _, _, err := ReadProfile(true, inputProfile) if (err != nil) { return false, err } if (ableToRead == false){ return false, nil } return true, nil } func VerifyProfileHash(inputHash [28]byte, profileTypeProvided bool, expectedProfileType string, isDisabledProvided bool, expectedIsDisabled bool)(bool, error){ if (profileTypeProvided == true){ if (expectedProfileType != "Mate" && expectedProfileType != "Host" && expectedProfileType != "Moderator"){ return false, errors.New("VerifyProfileHash called with invalid provided expectedProfileType: " + expectedProfileType) } } profileType, profileIsDisabled, err := ReadProfileHashMetadata(inputHash) if (err != nil){ return false, nil } if (profileTypeProvided == true){ if (profileType != expectedProfileType){ return false, nil } } if (isDisabledProvided == true){ if (profileIsDisabled != expectedIsDisabled){ return false, nil } } return true, nil } func VerifyAttributeHash(inputHash [27]byte, profileTypeProvided bool, expectedProfileType string, isCanonicalProvided bool, expectedIsCanonical bool)(bool, error){ if (profileTypeProvided == true){ if (expectedProfileType != "Mate" && expectedProfileType != "Host" && expectedProfileType != "Moderator"){ return false, errors.New("VerifyAttributeHash called with invalid provided expectedProfileType: " + expectedProfileType) } } profileType, attributeIsCanonical, err := ReadAttributeHashMetadata(inputHash) if (err != nil){ return false, nil } if (profileTypeProvided == true){ if (profileType != expectedProfileType){ return false, nil } } if (isCanonicalProvided == true){ if (attributeIsCanonical != expectedIsCanonical){ return false, nil } } return true, nil } //Outputs: // -string: Profile Type // -bool: Profile is disabled // -error: func ReadProfileHashMetadata(profileHash [28]byte)(string, bool, error){ metadataByte := profileHash[27] switch metadataByte{ case 1:{ return "Mate", true, nil } case 2:{ return "Mate", false, nil } case 3:{ return "Host", true, nil } case 4:{ return "Host", false, nil } case 5:{ return "Moderator", true, nil } case 6:{ return "Moderator", false, nil } } profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) return "", false, errors.New("ReadProfileHashMetadata called with invalid profileHash: " + profileHashHex) } //Outputs: // -string: Identity Type of author // -bool: Attribute is canonical // -error: func ReadAttributeHashMetadata(attributeHash [27]byte)(string, bool, error){ metadataByte := attributeHash[26] switch metadataByte{ case 1:{ return "Mate", true, nil } case 2:{ return "Mate", false, nil } case 3:{ return "Host", true, nil } case 4:{ return "Host", false, nil } case 5:{ return "Moderator", true, nil } case 6:{ return "Moderator", false, nil } } attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) return "", false, errors.New("ReadAttributeHashMetadata called with invalid attributeHash: " + attributeHashHex) } // This function will read a profile and compute its hash. //Outputs: // -bool: Able to read profile // -[28]byte: Profile hash // -int: Profile version // -byte: Network type (1 == Mainnet, 2 == Testnet1) // -[16]byte: Profile author identity hash // -int64: Profile broadcast time (alleged, can be faked) // -bool: Profile is disabled // -map[int]messagepack.RawMessage: Raw profile map (Attribute Identifier -> Attribute messagepack bytes value) // -error (will return err if there is a bug) func ReadProfileAndHash(verifyProfile bool, inputProfile []byte)(bool, [28]byte, int, byte, [16]byte, int64, bool, map[int]messagepack.RawMessage, error){ ableToRead, profileVersion, networkType, profileAuthor, profileBroadcastTime, profileIsDisabled, rawProfileMap, err := ReadProfile(verifyProfile, inputProfile) if (err != nil) { return false, [28]byte{}, 0, 0, [16]byte{}, 0, false, nil, err } if (ableToRead == false){ return false, [28]byte{}, 0, 0, [16]byte{}, 0, false, nil, nil } profileHashWithoutMetadataByte, err := blake3.GetBlake3HashAsBytes(27, inputProfile) if (err != nil) { return false, [28]byte{}, 0, 0, [16]byte{}, 0, false, nil, err } profileAuthorIdentityType, err := identity.GetIdentityTypeFromIdentityHash(profileAuthor) if (err != nil) { return false, [28]byte{}, 0, 0, [16]byte{}, 0, false, nil, err } getProfileHashMetadataByte := func()byte{ if (profileAuthorIdentityType == "Mate"){ if (profileIsDisabled == true){ return 1 } return 2 } else if (profileAuthorIdentityType == "Host"){ if (profileIsDisabled == true){ return 3 } return 4 } // profileAuthorIdentityType == "Moderator" if (profileIsDisabled == true){ return 5 } return 6 } profileHashMetadataByte := getProfileHashMetadataByte() profileHashSlice := append(profileHashWithoutMetadataByte, profileHashMetadataByte) profileHash := [28]byte(profileHashSlice) return true, profileHash, profileVersion, networkType, profileAuthor, profileBroadcastTime, profileIsDisabled, rawProfileMap, nil } // This function reads a profile without computing the profile's hash // This is faster because the profile's bytes do not need to be hashed //Outputs: // -bool: Able to read profile // -int: Profile version // -byte: Network type (1 == Mainnet, 2 == Testnet1) // -[16]byte: Profile author identity hash // -int64: Profile broadcast time (alleged, can be faked) // -bool: Profile is disabled // -map[int]messagepack.RawMessage: Raw profile map (Attribute identifier -> Attribute raw bytes) // -error (will return err if there is a bug) func ReadProfile(verifyProfile bool, inputProfile []byte)(bool, int, byte, [16]byte, int64, bool, map[int]messagepack.RawMessage, error){ var profileSlice []messagepack.RawMessage err := messagepack.Unmarshal(inputProfile, &profileSlice) if (err != nil){ // Profile is malformed: Invalid profile messagepack return false, 0, 0, [16]byte{}, 0, false, nil, nil } if (len(profileSlice) != 2){ // Profile is malformed: Invalid profile messagepack return false, 0, 0, [16]byte{}, 0, false, nil, nil } profileSignatureEncoded := profileSlice[0] profileContentEncoded := profileSlice[1] profileSignature, err := encoding.DecodeRawMessagePackTo64ByteArray(profileSignatureEncoded) if (err != nil){ // Profile is malformed: Invalid profile signature return false, 0, 0, [16]byte{}, 0, false, nil, nil } // This map will contain the profile content // The fields are encoded as identifiers, which need to be converted to names // Identifiers are integers, which we use instead of encoding the full name of the attribute // We do this to make profiles smaller // For example, 3 == "ProfileType" rawProfileMap := make(map[int]messagepack.RawMessage) err = encoding.DecodeMessagePackBytes(false, profileContentEncoded, &rawProfileMap) if (err != nil) { // Profile is malformed: Profile content map is invalid return false, 0, 0, [16]byte{}, 0, false, nil, nil } profileVersionEncoded, exists := rawProfileMap[1] if (exists == false) { // Profile is malformed: Missing profileVersion return false, 0, 0, [16]byte{}, 0, false, nil, nil } profileVersion, err := encoding.DecodeRawMessagePackToInt(profileVersionEncoded) if (err != nil){ // Profile is malformed: Invalid profile version return false, 0, 0, [16]byte{}, 0, false, nil, nil } if (profileVersion != 1){ // We cannot read this profile. It was created by an newer version of Seekia. return false, 0, 0, [16]byte{}, 0, false, nil, nil } networkTypeEncoded, exists := rawProfileMap[51] if (exists == false) { // Profile is malformed: Missing NetworkType return false, 0, 0, [16]byte{}, 0, false, nil, nil } networkType, err := encoding.DecodeRawMessagePackToByte(networkTypeEncoded) if (err != nil){ // Profile is malformed: Invalid network type return false, 0, 0, [16]byte{}, 0, false, nil, nil } isValid := helpers.VerifyNetworkType(networkType) if (isValid == false){ // Profile is malformed: Invalid network type return false, 0, 0, [16]byte{}, 0, false, nil, nil } identityKeyEncoded, exists := rawProfileMap[2] if (exists == false) { // Profile is malformed: Missing identity key. return false, 0, 0, [16]byte{}, 0, false, nil, nil } identityKey, err := encoding.DecodeRawMessagePackTo32ByteArray(identityKeyEncoded) if (err != nil){ // Profile is malformed: Invalid identity key return false, 0, 0, [16]byte{}, 0, false, nil, nil } profileTypeEncoded, exists := rawProfileMap[3] if (exists == false) { // Profile is malformed: missing ProfileType. return false, 0, 0, [16]byte{}, 0, false, nil, nil } profileType, err := encoding.DecodeRawMessagePackToString(profileTypeEncoded) if (err != nil){ // Profile is malformed: Invalid ProfileType. return false, 0, 0, [16]byte{}, 0, false, nil, nil } if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ // Profile is malformed: Invalid ProfileType. return false, 0, 0, [16]byte{}, 0, false, nil, nil } identityHash, err := identity.ConvertIdentityKeyToIdentityHash(identityKey, profileType) if (err != nil) { return false, 0, 0, [16]byte{}, 0, false, nil, err } broadcastTimeEncoded, exists := rawProfileMap[4] if (exists == false) { // Profile is malformed: missing BroadcastTime return false, 0, 0, [16]byte{}, 0, false, nil, nil } broadcastTimeInt64, err := encoding.DecodeRawMessagePackToInt64(broadcastTimeEncoded) if (err != nil) { // Profile is malformed: Contains invalid BroadcastTime return false, 0, 0, [16]byte{}, 0, false, nil, nil } if (verifyProfile == true){ isValid := helpers.VerifyBroadcastTime(broadcastTimeInt64) if (isValid == false){ // Profile is malformed: Contains invalid BroadcastTime return false, 0, 0, [16]byte{}, 0, false, nil, nil } } if (verifyProfile == true){ contentHash, err := blake3.Get32ByteBlake3Hash(profileContentEncoded) if (err != nil) { return false, 0, 0, [16]byte{}, 0, false, nil, err } signatureIsValid := edwardsKeys.VerifySignature(identityKey, profileSignature, contentHash) if (signatureIsValid == false) { // Profile is malformed: Invalid signature. return false, 0, 0, [16]byte{}, 0, false, nil, nil } } isDisabledEncoded, exists := rawProfileMap[5] if (exists == true){ // Profile is disabled. isDisabledBool, err := encoding.DecodeRawMessagePackToBool(isDisabledEncoded) if (err != nil){ // Profile is malformed: Invalid IsDisabled attribute. return false, 0, 0, [16]byte{}, 0, false, nil, nil } if (isDisabledBool == false){ // Profile is malformed: Invalid Disabled attribute. return false, 0, 0, [16]byte{}, 0, false, nil, nil } if (len(rawProfileMap) != 6){ // Profile is malformed: Invalid Disabled profile. return false, 0, 0, [16]byte{}, 0, false, nil, nil } return true, profileVersion, networkType, identityHash, broadcastTimeInt64, true, rawProfileMap, nil } if (verifyProfile == true){ profileAttributeObjectsList, err := profileFormat.GetProfileAttributeObjectsList() if (err != nil) { return false, 0, 0, [16]byte{}, 0, false, nil, err } for _, attributeObject := range profileAttributeObjectsList{ attributeName := attributeObject.AttributeName if (attributeName == "Disabled" || attributeName == "ProfileType" || attributeName == "IdentityKey" || attributeName == "ProfileVersion" || attributeName == "NetworkType" || attributeName == "BroadcastTime"){ // We already verified these attributes continue } attributeProfileVersions := attributeObject.ProfileVersions isRelevantVersion := slices.Contains(attributeProfileVersions, profileVersion) if (isRelevantVersion == false){ // This attribute does not belong to this profile version // Profiles created in this version do not have this attribute continue } attributeProfileTypes, err := attributeObject.GetProfileTypes(profileVersion) if (err != nil) { return false, 0, 0, [16]byte{}, 0, false, nil, err } isRelevantProfileType := slices.Contains(attributeProfileTypes, profileType) if (isRelevantProfileType == false){ continue } attributeIdentifier := attributeObject.AttributeIdentifier attributeIsRequired, err := attributeObject.GetIsRequired(profileVersion) if (err != nil) { return false, 0, 0, [16]byte{}, 0, false, nil, err } attributeValueBytes, exists := rawProfileMap[attributeIdentifier] if (exists == false){ if (attributeIsRequired == true){ // Profile is malformed: Profile is missing a required attribute return false, 0, 0, [16]byte{}, 0, false, nil, nil } continue } attributeIsValid, attributeValueString, err := formatProfileAttributeRawMessagePackToString(attributeName, attributeValueBytes) if (err != nil) { return false, 0, 0, [16]byte{}, 0, false, nil, err } if (attributeIsValid == false){ // Profile is malformed: Profile contains an invalid attribute value return false, 0, 0, [16]byte{}, 0, false, nil, nil } attributeValueIsValid, _, err := attributeObject.CheckValueFunction(profileVersion, profileType, attributeValueString) if (err != nil){ return false, 0, 0, [16]byte{}, 0, false, nil, err } if (attributeValueIsValid == false){ // Profile is malformed: Profile contains an invalid attribute value return false, 0, 0, [16]byte{}, 0, false, nil, nil } if (attributeIsRequired == false){ // We make sure profile has all the mandatory attributes for this attribute // We don't need to perform this check if the attribute is required mandatoryAttributesList, err := attributeObject.GetMandatoryAttributes(profileVersion) if (err != nil){ return false, 0, 0, [16]byte{}, 0, false, nil, err } for _, mandatoryAttributeName := range mandatoryAttributesList{ mandatoryAttributeIdentifier, err := profileFormat.GetAttributeIdentifierFromAttributeName(mandatoryAttributeName) if (err != nil){ return false, 0, 0, [16]byte{}, 0, false, nil, errors.New("GetMandatoryAttributes returning unknown attribute name: " + mandatoryAttributeName) } _, exists := rawProfileMap[mandatoryAttributeIdentifier] if (exists == false){ // Profile is malformed: Missing a mandatory attribute. return false, 0, 0, [16]byte{}, 0, false, nil, nil } } } } } return true, profileVersion, networkType, identityHash, broadcastTimeInt64, false, rawProfileMap, nil } // This function must be called on profiles which have already been verified // Outputs: // -bool: Attribute exists // -string: Attribute Value (Formatted) // -error func GetFormattedProfileAttributeFromRawProfileMap(rawProfileMap map[int]messagepack.RawMessage, attributeName string)(bool, string, error){ attributeIdentifier, err := profileFormat.GetAttributeIdentifierFromAttributeName(attributeName) if (err != nil) { return false, "", errors.New("GetFormattedProfileAttributeFromRawProfileMap failed: " + err.Error()) } attributeValueBytes, exists := rawProfileMap[attributeIdentifier] if (exists == false){ return false, "", nil } attributeIsValid, attributeFormatted, err := formatProfileAttributeRawMessagePackToString(attributeName, attributeValueBytes) if (err != nil){ return false, "", err } if (attributeIsValid == false){ // Profile is malformed. // This should never happen, because this function should only be called after the profile has been verified by ReadProfile return false, "", errors.New("GetFormattedProfileAttributeFromRawProfileMap called with raw profile map containing invalid " + attributeName + " attribute value.") } return true, attributeFormatted, nil } // We use this function to convert attributes from raw MessagePack to string //Outputs: // -bool: Attribute is valid // -Be aware that this function does not fully verify attributes // -string: Attribute formatted // -error func formatProfileAttributeRawMessagePackToString(attributeName string, attributeValueBytes messagepack.RawMessage)(bool, string, error){ switch attributeName{ // Some attributes are encoded in special ways in MessagePack to save space // For example, BroadcastTime is encoded as a int64 rather than a string case "ProfileVersion":{ profileVersion, err := encoding.DecodeRawMessagePackToInt(attributeValueBytes) if (err != nil){ // Profile is malformed: Invalid profile version return false, "", nil } if (profileVersion != 1){ // We cannot read this profile. It was created by an newer version of Seekia. // This should never happen, because this function should only be called after the profile's network type has been verified return false, "", errors.New("formatProfileAttributeRawMessagePackToString called with unknown ProfileVersion.") } return true, "1", nil } case "NetworkType":{ networkType, err := encoding.DecodeRawMessagePackToByte(attributeValueBytes) if (err != nil){ // Profile is malformed: Invalid network type // This should never happen, because this function should only be called after the profile's networkType has been verified return false, "", errors.New("formatProfileAttributeRawMessagePackToString called with invalid NetworkType.") } isValid := helpers.VerifyNetworkType(networkType) if (isValid == false){ // Profile malformed: Invalid network type // This should never happen, because this function should only be called after the profile's networkType has been verified return false, "", errors.New("formatProfileAttributeRawMessagePackToString called with invalid NetworkType.") } networkTypeString := helpers.ConvertByteToString(networkType) return true, networkTypeString, nil } case "IdentityKey":{ identityKey, err := encoding.DecodeRawMessagePackTo32ByteArray(attributeValueBytes) if (err != nil){ // Profile is malformed: Invalid identity key // This should never happen, because this function should only be called after the profile's IdentityKey has been verified return false, "", errors.New("formatProfileAttributeRawMessagePackToString called with invalid IdentityKey.") } identityKeyHexString := encoding.EncodeBytesToHexString(identityKey[:]) return true, identityKeyHexString, nil } case "BroadcastTime":{ broadcastTimeInt64, err := encoding.DecodeRawMessagePackToInt64(attributeValueBytes) if (err != nil) { // Profile is malformed: Contains invalid BroadcastTime // This should never happen, because this function should only be called after the profile's BroadcastTime has been verified return false, "", errors.New("formatProfileAttributeRawMessagePackToString called with invalid BroadcastTime.") } broadcastTimeString := helpers.ConvertInt64ToString(broadcastTimeInt64) return true, broadcastTimeString, nil } case "Disabled":{ isDisabledBool, err := encoding.DecodeRawMessagePackToBool(attributeValueBytes) if (err != nil){ // Profile is malformed: Contains invalid Disabled attribute // This should never happen, because this function should only be called after the profile's Disabled has been verified return false, "", errors.New("formatProfileAttributeRawMessagePackToString called with invalid Disabled.") } if (isDisabledBool == false){ // Profile is malformed: Contains invalid Disabled attribute // This should never happen, because this function should only be called after the profile's Disabled has been verified return false, "", errors.New("formatProfileAttributeRawMessagePackToString called with invalid Disabled.") } return true, "Yes", nil } case "NaclKey", "KyberKey":{ keyBytes, err := encoding.DecodeRawMessagePackToBytes(attributeValueBytes) if (err != nil) { // Profile is malformed: Contains invalid NaclKey/KyberKey attribute return false, "", nil } keyString := encoding.EncodeBytesToBase64String(keyBytes) return true, keyString, nil } case "Photos":{ var rawPhotoBytesList [][]byte err := encoding.DecodeMessagePackBytes(false, attributeValueBytes, &rawPhotoBytesList) if (err != nil) { // Profile is malformed: Contains invalid Photos attribute return false, "", nil } if (len(rawPhotoBytesList) == 0){ // Profile is malformed: Contains invalid Photos attribute return false, "", nil } // We use this to build the output // The output is a "+" delimited string of each photo's bytes encoded in Base64 var attributeBuilder strings.Builder finalIndex := len(rawPhotoBytesList) - 1 for index, photoBytes := range rawPhotoBytesList{ photoBase64 := encoding.EncodeBytesToBase64String(photoBytes) attributeBuilder.WriteString(photoBase64) if (index != finalIndex){ attributeBuilder.WriteString("+") } } photoAttributeString := attributeBuilder.String() return true, photoAttributeString, nil } } // Attribute is not encoded in a special way. // It is encoded as a unicode string. attributeValueString, err := encoding.DecodeRawMessagePackToString(attributeValueBytes) if (err != nil) { return false, "", err } return true, attributeValueString, nil } // Attribute hashes are used for moderation // A profile's attribute hash can be reviewed without reviewing the whole profile // This function does not verify the raw profile map. The raw profile map must first be verified // Outputs: // -map[int][27]byte: Profile attribute hashes map (Attribute identifier -> Attribute hash) // -bool: Profile is canonical (all attributes are canonical) // -error func GetProfileAttributeHashesMap(profileAuthor [16]byte, profileVersion int, profileNetworkType byte, rawProfileMap map[int]messagepack.RawMessage)(map[int][27]byte, bool, error){ profileType, err := identity.GetIdentityTypeFromIdentityHash(profileAuthor) if (err != nil){ profileAuthorHex := encoding.EncodeBytesToHexString(profileAuthor[:]) return nil, false, errors.New("GetProfileAttributeHashesMap called with invalid profileAuthor: " + profileAuthorHex) } _, exists := rawProfileMap[5] if (exists == true){ // Profile is disabled. // A disabled profile has an empty attribute hashes map. // All disabled profile are also canonical emptyMap := make(map[int][27]byte) return emptyMap, true, nil } // We use a prefix when creating attribute hashes // The prefix starts with the string "profileattributehashsalt" decoded from base32 to bytes attributeHashInputPrefix := []byte{124, 92, 84, 44, 128, 156, 226, 128, 210, 100, 56, 36, 121, 1, 115} attributeHashInputPrefix = append(attributeHashInputPrefix, profileAuthor[:]...) attributeHashInputPrefix = append(attributeHashInputPrefix, profileNetworkType) // Map Structure: Attribute Identifier -> Attribute hash profileAttributeHashesMap := make(map[int][27]byte) // We set this variable to false if any attribute is not canonical profileIsCanonical := true for attributeIdentifier, attributeRawValueBytes := range rawProfileMap{ attributeObject, err := profileFormat.GetAttributeObjectFromAttributeIdentifier(attributeIdentifier) if (err != nil) { return nil, false, err } attributeName := attributeObject.AttributeName if (attributeName == "ProfileType" || attributeName == "IdentityKey" || attributeName == "ProfileVersion" || attributeName == "NetworkType"){ // We don't need to store these attributes in the attribute hashes map continue } //Outputs: // -bool: Attribute is canonical // -error getAttributeIsCanonical := func()(bool, error){ getIsCanonicalFunction := attributeObject.GetIsCanonical attributeIsCanonicalInfo, err := getIsCanonicalFunction(profileVersion) if (err != nil) { return false, err } if (attributeIsCanonicalInfo == "Always"){ return true, nil } if (attributeIsCanonicalInfo == "Never"){ return false, nil } // attributeIsCanonicalInfo == "Sometimes" // We have to manually check to see if the attribute's value is canonical valueIsValid, attributeValueString, err := formatProfileAttributeRawMessagePackToString(attributeName, attributeRawValueBytes) if (err != nil) { return false, err } if (valueIsValid == false){ // Profile contains an invalid attribute value return false, errors.New("GetProfileAttributeHashesMap called with profile containing invalid attribute value for attribute: " + attributeName) } valueIsValid, attributeIsCanonical, err := attributeObject.CheckValueFunction(profileVersion, profileType, attributeValueString) if (err != nil){ return false, err } if (valueIsValid == false){ // Profile contains an invalid attribute value return false, errors.New("GetProfileAttributeHashesMap called with profile containing invalid attribute value for attribute: " + attributeName + ". Attribute value: " + attributeValueString) } return attributeIsCanonical, nil } attributeIsCanonical, err := getAttributeIsCanonical() if (err != nil) { return nil, false, err } if (attributeIsCanonical == false){ profileIsCanonical = false } // We now create the attribute hash // Each attribute hash represents an attribute value created by a specific author // // We use the messagePack encoded bytes to create the attribute hash // I'm not sure, but it may be possible to encode the same value using multiple different messagepack encodings // For example, a number could be encoded in two different ways using raw messagepack // This would result in two different attribute hashes for the same attribute value // This is fine // It would only be a problem if an attribute hash represented two different attribute values if (attributeIdentifier < 1 || attributeIdentifier > 4294967295){ return nil, false, errors.New("profileFormat contains invalid attribute identifier.") } attributeIdentifierUint32 := uint32(attributeIdentifier) // We copy the prefix and append the attribute identifier and attribute raw value bytes // We then feed that into blake3 hashInputBytes := slices.Clone(attributeHashInputPrefix) hashInputBytes = binary.LittleEndian.AppendUint32(hashInputBytes, attributeIdentifierUint32) hashInputBytes = append(hashInputBytes, attributeRawValueBytes...) attributeHashWithoutMetadata, err := blake3.GetBlake3HashAsBytes(26, hashInputBytes) if (err != nil) { return nil, false, err } getMetadataByte := func()(byte, error){ if (profileType == "Mate"){ if (attributeIsCanonical == true){ return 1, nil } return 2, nil } if (profileType == "Host"){ if (attributeIsCanonical == true){ return 3, nil } return 4, nil } if (profileType == "Moderator"){ if (attributeIsCanonical == true){ return 5, nil } return 6, nil } return 0, errors.New("GetIdentityTypeFromIdentityHash returning invalid profileType: " + profileType) } attributeHashMetadataByte, err := getMetadataByte() if (err != nil) { return nil, false, err } attributeHashBytes := append(attributeHashWithoutMetadata, attributeHashMetadataByte) attributeHash := [27]byte(attributeHashBytes) profileAttributeHashesMap[attributeIdentifier] = attributeHash } return profileAttributeHashesMap, profileIsCanonical, nil }