Switch labeled hash to explicit formatting

This commit is contained in:
Daniel 2021-05-04 15:28:23 +02:00
parent af4dbad99f
commit 1ac81dde54
2 changed files with 118 additions and 22 deletions

View file

@ -3,9 +3,12 @@ package lhash
import ( import (
"crypto/subtle" "crypto/subtle"
"encoding/base64" "encoding/base64"
"encoding/hex"
"errors" "errors"
"fmt" "fmt"
"github.com/mr-tron/base58"
"github.com/safing/portbase/container" "github.com/safing/portbase/container"
) )
@ -60,11 +63,33 @@ func Load(labeledHash []byte) (*LabeledHash, error) {
}, nil }, nil
} }
// LoadFromString loads a labeled hash from the given string. // FromHex loads a labeled hash from the given hexadecimal string.
func LoadFromString(labeledHash string) (*LabeledHash, error) { func FromHex(hexEncoded string) (*LabeledHash, error) {
raw, err := base64.RawURLEncoding.DecodeString(labeledHash) raw, err := hex.DecodeString(hexEncoded)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to decode: %s", err) return nil, fmt.Errorf("failed to decode hex: %s", 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: %s", 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: %s", err)
} }
return Load(raw) return Load(raw)
@ -78,13 +103,37 @@ func (lh *LabeledHash) Bytes() []byte {
return c.CompileData() return c.CompileData()
} }
// String returns the string representation of the labeled hash (base64 raw url encoding). // Hex returns the hexadecimal string representation of the labeled hash.
func (lh *LabeledHash) String() string { 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()) return base64.RawURLEncoding.EncodeToString(lh.Bytes())
} }
// Matches returns true if the digest of the given data matches the hash. // Base58 returns the Base58 string representation of the labeled hash using
func (lh *LabeledHash) Matches(data []byte) bool { // 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
}
// MatchesString returns true if the digest of the given string matches the hash.
func (lh *LabeledHash) MatchesString(s string) bool {
return lh.MatchesData([]byte(s))
}
// MatchesData returns true if the digest of the given data matches the hash.
func (lh *LabeledHash) MatchesData(data []byte) bool {
hasher := lh.alg.new() hasher := lh.alg.new()
_, _ = hasher.Write(data) // never returns an error _, _ = hasher.Write(data) // never returns an error
defer hasher.Reset() // internal state may leak data if kept in memory defer hasher.Reset() // internal state may leak data if kept in memory

View file

@ -7,8 +7,11 @@ import (
) )
var ( var (
testEmpty = []byte("") testEmpty = []byte("")
testFox = []byte("The quick brown fox jumps over the lazy dog.") testFox = "The quick brown fox jumps over the lazy dog."
testFoxData = []byte(testFox)
noMatch = "no match"
noMatchData = []byte(noMatch)
) )
func testAlgorithm(t *testing.T, alg Algorithm, emptyHex, foxHex string) { func testAlgorithm(t *testing.T, alg Algorithm, emptyHex, foxHex string) {
@ -32,33 +35,77 @@ func testAlgorithm(t *testing.T, alg Algorithm, emptyHex, foxHex string) {
} }
// test fox // test fox
lh = Digest(alg, testFox) lh = Digest(alg, testFoxData)
if !bytes.Equal(lh.Bytes()[2:], foxBytes) { if !bytes.Equal(lh.Bytes()[2:], foxBytes) {
t.Errorf("alg %d: test fox: digest mismatch, expected %+v, got %+v", alg, foxBytes, lh.Bytes()[2:]) t.Errorf("alg %d: test fox: digest mismatch, expected %+v, got %+v", alg, foxBytes, lh.Bytes()[2:])
} }
// test matching // test matching with serialized/loaded labeled hash
if !lh.Matches(testFox) { if !lh.MatchesData(testFoxData) {
t.Errorf("alg %d: failed to match reference", alg) t.Errorf("alg %d: failed to match reference", alg)
} }
if lh.Matches([]byte("nope")) { if !lh.MatchesString(testFox) {
t.Errorf("alg %d: failed to match reference", alg)
}
if lh.MatchesData(noMatchData) {
t.Errorf("alg %d: failed to non-match garbage", alg)
}
if lh.MatchesString(noMatch) {
t.Errorf("alg %d: failed to non-match garbage", alg) t.Errorf("alg %d: failed to non-match garbage", alg)
} }
// serialize // Test representations
lhs := Digest(alg, testFox).String()
// load // Hex
loaded, err := LoadFromString(lhs) lhs := Digest(alg, testFoxData)
loaded, err := FromHex(lhs.Hex())
if err != nil { if err != nil {
t.Errorf("alg %d: failed to load from string: %s", alg, err) t.Errorf("alg %d: failed to load from hex string: %s", alg, err)
return return
} }
testFormat(t, alg, lhs, loaded)
// test matching with serialized/loaded labeled hash // Base64
if !loaded.Matches(testFox) { lhs = Digest(alg, testFoxData)
loaded, err = FromBase64(lhs.Base64())
if err != nil {
t.Errorf("alg %d: failed to load from base64 string: %s", alg, err)
return
}
testFormat(t, alg, lhs, loaded)
// Base58
lhs = Digest(alg, testFoxData)
loaded, err = FromBase58(lhs.Base58())
if err != nil {
t.Errorf("alg %d: failed to load from base58 string: %s", alg, err)
return
}
testFormat(t, alg, lhs, loaded)
}
func testFormat(t *testing.T, alg Algorithm, lhs, loaded *LabeledHash) {
noMatchLH := Digest(alg, noMatchData)
// Test equality.
if !lhs.Equal(loaded) {
t.Errorf("alg %d: equality test failed", alg)
}
if lhs.Equal(noMatchLH) {
t.Errorf("alg %d: non-equality test failed", alg)
}
// Test matching.
if !loaded.MatchesData(testFoxData) {
t.Errorf("alg %d: failed to match reference", alg) t.Errorf("alg %d: failed to match reference", alg)
} }
if loaded.Matches([]byte("nope")) { if !loaded.MatchesString(testFox) {
t.Errorf("alg %d: failed to match reference", alg)
}
if loaded.MatchesData(noMatchData) {
t.Errorf("alg %d: failed to non-match garbage", alg)
}
if loaded.MatchesString(noMatch) {
t.Errorf("alg %d: failed to non-match garbage", alg) t.Errorf("alg %d: failed to non-match garbage", alg)
} }
} }