2024-04-11 15:51:56 +02:00
// 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
2024-06-11 06:59:06 +02:00
// -int64: Profile creation time (alleged, can be faked)
2024-04-11 15:51:56 +02:00
// -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 ) {
2024-06-11 06:59:06 +02:00
ableToRead , profileVersion , networkType , profileAuthor , profileCreationTime , profileIsDisabled , rawProfileMap , err := ReadProfile ( verifyProfile , inputProfile )
2024-04-11 15:51:56 +02:00
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 )
2024-06-11 06:59:06 +02:00
return true , profileHash , profileVersion , networkType , profileAuthor , profileCreationTime , profileIsDisabled , rawProfileMap , nil
2024-04-11 15:51:56 +02:00
}
// 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
2024-06-11 06:59:06 +02:00
// -int64: Profile creation time (alleged, can be faked)
2024-04-11 15:51:56 +02:00
// -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 }
2024-06-11 06:59:06 +02:00
creationTimeEncoded , exists := rawProfileMap [ 4 ]
2024-04-11 15:51:56 +02:00
if ( exists == false ) {
2024-06-11 06:59:06 +02:00
// Profile is malformed: missing CreationTime
2024-04-11 15:51:56 +02:00
return false , 0 , 0 , [ 16 ] byte { } , 0 , false , nil , nil
}
2024-06-11 06:59:06 +02:00
creationTimeInt64 , err := encoding . DecodeRawMessagePackToInt64 ( creationTimeEncoded )
2024-04-11 15:51:56 +02:00
if ( err != nil ) {
2024-06-11 06:59:06 +02:00
// Profile is malformed: Contains invalid CreationTime
2024-04-11 15:51:56 +02:00
return false , 0 , 0 , [ 16 ] byte { } , 0 , false , nil , nil
}
if ( verifyProfile == true ) {
2024-06-11 06:59:06 +02:00
isValid := helpers . VerifyCreationTime ( creationTimeInt64 )
2024-04-11 15:51:56 +02:00
if ( isValid == false ) {
2024-06-11 06:59:06 +02:00
// Profile is malformed: Contains invalid CreationTime
2024-04-11 15:51:56 +02:00
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
}
2024-06-11 06:59:06 +02:00
return true , profileVersion , networkType , identityHash , creationTimeInt64 , true , rawProfileMap , nil
2024-04-11 15:51:56 +02:00
}
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
2024-06-11 06:59:06 +02:00
if ( attributeName == "Disabled" || attributeName == "ProfileType" || attributeName == "IdentityKey" || attributeName == "ProfileVersion" || attributeName == "NetworkType" || attributeName == "CreationTime" ) {
2024-04-11 15:51:56 +02:00
// 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
}
}
}
}
}
2024-06-11 06:59:06 +02:00
return true , profileVersion , networkType , identityHash , creationTimeInt64 , false , rawProfileMap , nil
2024-04-11 15:51:56 +02:00
}
// 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
2024-06-11 06:59:06 +02:00
// For example, CreationTime is encoded as a int64 rather than a string
2024-04-11 15:51:56 +02:00
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
}
2024-06-11 06:59:06 +02:00
case "CreationTime" : {
2024-04-11 15:51:56 +02:00
2024-06-11 06:59:06 +02:00
creationTimeInt64 , err := encoding . DecodeRawMessagePackToInt64 ( attributeValueBytes )
2024-04-11 15:51:56 +02:00
if ( err != nil ) {
2024-06-11 06:59:06 +02:00
// Profile is malformed: Contains invalid CreationTime
// This should never happen, because this function should only be called after the profile's CreationTime has been verified
return false , "" , errors . New ( "formatProfileAttributeRawMessagePackToString called with invalid CreationTime." )
2024-04-11 15:51:56 +02:00
}
2024-06-11 06:59:06 +02:00
creationTimeString := helpers . ConvertInt64ToString ( creationTimeInt64 )
2024-04-11 15:51:56 +02:00
2024-06-11 06:59:06 +02:00
return true , creationTimeString , nil
2024-04-11 15:51:56 +02:00
}
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
}