822 lines
27 KiB
Go
822 lines
27 KiB
Go
|
|
// 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
|
|
}
|
|
|
|
|