diff --git a/swupd/hash.go b/swupd/hash.go index 6e42260..75c3a11 100644 --- a/swupd/hash.go +++ b/swupd/hash.go @@ -20,7 +20,7 @@ import ( "encoding/hex" "fmt" "hash" - "io/ioutil" + "io" "os" "syscall" ) @@ -58,64 +58,14 @@ func HashEquals(h1 Hashval, h2 Hashval) bool { // Hashcalc returns the swupd hash for the given file func Hashcalc(filename string) (Hashval, error) { - var info syscall.Stat_t - var err error - var data []byte - if err = syscall.Lstat(filename, &info); err != nil { - return 0, fmt.Errorf("error statting file '%s' %v", filename, err) + r, err := GetHashForFile(filename) + if err != nil { + return 0, err } - // Get magic constants out of /usr/include/bits/stat.h - switch info.Mode & syscall.S_IFMT { - case syscall.S_IFREG: // Regular file - data, err = ioutil.ReadFile(filename) - if err != nil { - return 0, fmt.Errorf("read error for '%s' %v", filename, err) - } - case syscall.S_IFDIR: // Directory - info.Size = 0 - data = []byte("DIRECTORY") // fixed magic string - case syscall.S_IFLNK: - info.Mode = 0 - target, err := os.Readlink(filename) - if err != nil { - return 0, fmt.Errorf("error readlink file '%s' %v", filename, err) - } - data = []byte(target) - default: - return 0, fmt.Errorf("%s is not a file, directory or symlink %o", filename, info.Mode&syscall.S_IFMT) - } - r := internHash(genHash(info, data)) - return r, nil + return internHash(r), nil } -// genHash generates hash string from butchered Stat_t and data -// Expects that its callers have validated the arguments -func genHash(info syscall.Stat_t, data []byte) string { - key := hmacComputeKey(info) - result := hmacSha256ForData(key, data) - return string(result[:]) -} - -// hmacSha256ForData returns an array of 64 ascii hex digits -func hmacSha256ForData(key []byte, data []byte) []byte { - var result [64]byte - - mac := hmac.New(sha256.New, key) - _, _ = mac.Write(data) - hex.Encode(result[:], mac.Sum(nil)) - return result[:] -} - -// This is what I want to have for the key -// type updatestat struct { -// st_mode uint64 -// st_uid uint64 -// st_gid uint64 -// st_rdev uint64 -// st_size uint64 -// } - -// set fills in a buffer with an int in little endian order +// set fills in a buffer with an int in little endian order. func set(out []byte, in int64) { for i := range out { out[i] = byte(in & 0xff) @@ -123,24 +73,6 @@ func set(out []byte, in int64) { } } -// hmacComputeKey returns what should be an ascii string as an array of byte -// it is really ugly to be compatible with the C implementation. It is not portable -// as the C version isn't portable. -// The syscall.Stat_t has been butchered -func hmacComputeKey(info syscall.Stat_t) []byte { - // Create the key - updatestat := [40]byte{} - set(updatestat[0:8], int64(info.Mode)) - set(updatestat[8:16], int64(info.Uid)) - set(updatestat[16:24], int64(info.Gid)) - // 24:32 is rdev, but this is always zero - set(updatestat[24:32], 0) - set(updatestat[32:40], int64(info.Size)) - // fmt.Printf("key is %v\n", updatestat) - key := hmacSha256ForData(updatestat[:], nil) - return key -} - // HashFileInfo contains the metadata of a file that is included as // part of the swupd hash. type HashFileInfo struct { @@ -217,7 +149,7 @@ func NewHash(info *HashFileInfo) (*Hash, error) { hmac: hmac.New(sha256.New, key[:]), } - // Pre-write data we known so that directories and symbolic + // Pre-write data we know so that directories and symbolic // links don't need further data from the caller. if data != nil { _, err = h.hmac.Write(data) @@ -241,3 +173,63 @@ func (h *Hash) Sum() string { hex.Encode(result[:], h.hmac.Sum(nil)) return string(result[:]) } + +// GetHashForFile calculate the swupd hash for a file in the disk. +func GetHashForFile(filename string) (string, error) { + var info syscall.Stat_t + var err error + if err = syscall.Lstat(filename, &info); err != nil { + return "", fmt.Errorf("error statting file '%s' %v", filename, err) + } + + hashInfo := &HashFileInfo{ + Mode: info.Mode, + UID: info.Uid, + GID: info.Gid, + Size: info.Size, + } + + if info.Mode&syscall.S_IFMT == syscall.S_IFLNK { + var link string + link, err = os.Readlink(filename) + if err != nil { + return "", err + } + hashInfo.Linkname = link + } + + h, err := NewHash(hashInfo) + if err != nil { + return "", fmt.Errorf("error creating hash for file %s: %s", filename, err) + } + + if info.Mode&syscall.S_IFMT == syscall.S_IFREG { + f, err := os.Open(filename) + if err != nil { + return "", fmt.Errorf("read error for file %s: %s", filename, err) + } + _, err = io.Copy(h, f) + _ = f.Close() + if err != nil { + return "", fmt.Errorf("error hashing file %s: %s", filename, err) + } + } + + return h.Sum(), nil +} + +// GetHashForBytes calculate the hash for data already in memory and the +// associated metadata. +func GetHashForBytes(info *HashFileInfo, data []byte) (string, error) { + h, err := NewHash(info) + if err != nil { + return "", err + } + if data != nil { + _, err = h.Write(data) + if err != nil { + return "", err + } + } + return h.Sum(), nil +} diff --git a/swupd/hash_test.go b/swupd/hash_test.go index d848707..9b18045 100644 --- a/swupd/hash_test.go +++ b/swupd/hash_test.go @@ -3,7 +3,6 @@ package swupd import ( "fmt" "os" - "syscall" "testing" ) @@ -91,33 +90,49 @@ const ( Reg = 0100000 ) -// TestGenHash checks that the internal data hashing is correct -func TestGenHash(t *testing.T) { +func TestGetHashForBytes(t *testing.T) { testCases := []struct { - info syscall.Stat_t + name string + info HashFileInfo data []byte result string }{ - {syscall.Stat_t{Mode: (Dir + 0755)}, - []byte("DIRECTORY"), directoryhash}, - {syscall.Stat_t{Mode: (Dir + 01777)}, - []byte("DIRECTORY"), - "d93a5e9129361e28b9e244fe422234e3a1794b001a082aeb78e16fd881673a2b"}, - {syscall.Stat_t{Mode: Reg + 0644, Uid: 1000, Gid: 1000}, - []byte(""), - "b85f1dc2c2317a20f47a36d3257313b131124ffa6d4f19bb060d43014fd386b0"}, - {syscall.Stat_t{Mode: Reg + 0644, Uid: 1000, Gid: 201}, - []byte(""), - "0a3978d8b6ea47b779a2dfb5d6a7f57c93d28e131870bcd187470da3678d1298"}, - {syscall.Stat_t{Mode: Reg + 0644, Uid: 1000, Gid: 201, Size: 6}, - []byte("hello\n"), - "53b40563c1162a14d9ce0233a6b346cd0a4cbce54c40affbdf0fc286fd3bfe7b"}, + { + name: "directory", + info: HashFileInfo{Mode: Dir + 0755}, + result: directoryhash, + }, + { + name: "directory with different permissions", + info: HashFileInfo{Mode: Dir + 01777}, + result: "d93a5e9129361e28b9e244fe422234e3a1794b001a082aeb78e16fd881673a2b", + }, + { + name: "empty regular user file 0644", + info: HashFileInfo{Mode: Reg + 0644, UID: 1000, GID: 1000}, + result: "b85f1dc2c2317a20f47a36d3257313b131124ffa6d4f19bb060d43014fd386b0", + }, + { + name: "empty regular user file 0644 with different group", + info: HashFileInfo{Mode: Reg + 0644, UID: 1000, GID: 201}, + result: "0a3978d8b6ea47b779a2dfb5d6a7f57c93d28e131870bcd187470da3678d1298", + }, + { + name: "regular user file with different group", + info: HashFileInfo{Mode: Reg + 0644, UID: 1000, GID: 201, Size: 6}, + data: []byte("hello\n"), + result: "53b40563c1162a14d9ce0233a6b346cd0a4cbce54c40affbdf0fc286fd3bfe7b", + }, } for _, tc := range testCases { - r := genHash(tc.info, tc.data) - if r != tc.result { - t.Errorf("Unexpected result %s for\n%v\n", r, tc) + hash, err := GetHashForBytes(&tc.info, tc.data) + if err != nil { + t.Errorf("couldn't calculate hash for case %q: %s", tc.name, err) + continue + } + if hash != tc.result { + t.Errorf("Unexpected result for case %s, got %s but wanted %s", tc.name, hash, tc.result) } } }