package lhash import ( "bufio" "crypto/subtle" "encoding/base64" "encoding/hex" "errors" "fmt" "io" "os" "github.com/mr-tron/base58" "github.com/safing/structures/container" ) // LabeledHash represents a typed hash value. type LabeledHash struct { alg Algorithm digest []byte } // Digest creates a new labeled hash and digests the given data. func Digest(alg Algorithm, data []byte) *LabeledHash { hasher := alg.new() _, _ = hasher.Write(data) // Never returns an error. defer hasher.Reset() // Internal state may leak data if kept in memory. return &LabeledHash{ alg: alg, digest: hasher.Sum(nil), } } // DigestFile creates a new labeled hash and digests the given file. func DigestFile(alg Algorithm, pathToFile string) (*LabeledHash, error) { // Open file that should be hashed. file, err := os.Open(pathToFile) if err != nil { return nil, fmt.Errorf("failed to open file: %w", err) } return DigestFromReader(alg, file) } // DigestFromReader creates a new labeled hash and digests from the given reader. func DigestFromReader(alg Algorithm, reader io.Reader) (*LabeledHash, error) { hasher := alg.new() defer hasher.Reset() // Internal state may leak data if kept in memory. // Pipe all data directly to the hashing algorithm. _, err := bufio.NewReader(reader).WriteTo(hasher) if err != nil { return nil, fmt.Errorf("failed to read: %w", err) } return &LabeledHash{ alg: alg, digest: hasher.Sum(nil), }, nil } // Load loads a labeled hash from the given []byte slice. func Load(labeledHash []byte) (*LabeledHash, error) { c := container.New(labeledHash) algID, err := c.GetNextN64() if err != nil { return nil, fmt.Errorf("failed to parse algorithm ID: %w", err) } digest, err := c.GetNextBlock() if err != nil { return nil, fmt.Errorf("failed to parse digest: %w", err) } if c.Length() > 0 { return nil, errors.New("integrity error: data left over after parsing") } alg := Algorithm(uint(algID)) if alg.new() == nil { return nil, errors.New("compatibility error: invalid or unsupported algorithm") } if alg.new().Size() != len(digest) { return nil, errors.New("integrity error: invalid digest length") } return &LabeledHash{ alg: alg, digest: digest, }, nil } // FromHex loads a labeled hash from the given hexadecimal string. func FromHex(hexEncoded string) (*LabeledHash, error) { raw, err := hex.DecodeString(hexEncoded) if err != nil { return nil, fmt.Errorf("failed to decode hex: %w", err) } return Load(raw) } // FromBase64 loads a labeled hash from the given Base64 string using raw url // encoding. func FromBase64(base64Encoded string) (*LabeledHash, error) { raw, err := base64.RawURLEncoding.DecodeString(base64Encoded) if err != nil { return nil, fmt.Errorf("failed to decode base64: %w", err) } return Load(raw) } // FromBase58 loads a labeled hash from the given Base58 string using the BTC // alphabet. func FromBase58(base58Encoded string) (*LabeledHash, error) { raw, err := base58.Decode(base58Encoded) if err != nil { return nil, fmt.Errorf("failed to decode base58: %w", err) } return Load(raw) } // Algorithm returns the algorithm of the labeled hash. func (lh *LabeledHash) Algorithm() Algorithm { return lh.alg } // Sum returns the raw calculated hash digest. func (lh *LabeledHash) Sum() []byte { return lh.digest } // Bytes return the []byte representation of the labeled hash. func (lh *LabeledHash) Bytes() []byte { c := container.New() c.AppendNumber(uint64(lh.alg)) c.AppendAsBlock(lh.digest) return c.CompileData() } // Hex returns the hexadecimal string representation of the labeled hash. func (lh *LabeledHash) Hex() string { return hex.EncodeToString(lh.Bytes()) } // Base64 returns the Base64 string representation of the labeled hash using // raw url encoding. func (lh *LabeledHash) Base64() string { return base64.RawURLEncoding.EncodeToString(lh.Bytes()) } // Base58 returns the Base58 string representation of the labeled hash using // the BTC alphabet. func (lh *LabeledHash) Base58() string { return base58.Encode(lh.Bytes()) } // Equal returns true if the given labeled hash is equal. // Equality is checked by comparing both the algorithm and the digest value. func (lh *LabeledHash) Equal(other *LabeledHash) bool { return lh.alg == other.alg && subtle.ConstantTimeCompare(lh.digest, other.digest) == 1 } // EqualRaw returns true if the given raw hash digest is equal. // Equality is checked by comparing both the digest value only. // The caller must make sure the same algorithm is used. func (lh *LabeledHash) EqualRaw(otherDigest []byte) bool { return subtle.ConstantTimeCompare(lh.digest, otherDigest) == 1 } // Matches returns true if the digest of the given data matches the hash. func (lh *LabeledHash) Matches(data []byte) bool { return lh.Equal(Digest(lh.alg, data)) } // MatchesData returns true if the digest of the given data matches the hash. // Deprecated: Use Matches instead. func (lh *LabeledHash) MatchesData(data []byte) bool { return lh.Equal(Digest(lh.alg, data)) } // MatchesString returns true if the digest of the given string matches the hash. func (lh *LabeledHash) MatchesString(s string) bool { return lh.Matches([]byte(s)) } // MatchesFile returns true if the digest of the given file matches the hash. func (lh *LabeledHash) MatchesFile(pathToFile string) (bool, error) { fileHash, err := DigestFile(lh.alg, pathToFile) if err != nil { return false, err } return lh.Equal(fileHash), nil } // MatchesReader returns true if the digest of the given reader matches the hash. func (lh *LabeledHash) MatchesReader(reader io.Reader) (bool, error) { readerHash, err := DigestFromReader(lh.alg, reader) if err != nil { return false, err } return lh.Equal(readerHash), nil }