diff --git a/fst/id.go b/fst/id.go new file mode 100644 index 0000000..a8363c2 --- /dev/null +++ b/fst/id.go @@ -0,0 +1,48 @@ +package fst + +import ( + "crypto/rand" + "encoding/hex" + "errors" + "fmt" +) + +type ID [16]byte + +var ( + ErrInvalidLength = errors.New("string representation must have a length of 32") +) + +func (a *ID) String() string { + return hex.EncodeToString(a[:]) +} + +func NewAppID(id *ID) error { + _, err := rand.Read(id[:]) + return err +} + +func ParseAppID(id *ID, s string) error { + if len(s) != 32 { + return ErrInvalidLength + } + + for i, b := range s { + if b < '0' || b > 'f' { + return fmt.Errorf("invalid char %q at byte %d", b, i) + } + + v := uint8(b) + if v > '9' { + v = 10 + v - 'a' + } else { + v -= '0' + } + if i%2 == 0 { + v <<= 4 + } + id[i/2] += v + } + + return nil +} diff --git a/fst/id_test.go b/fst/id_test.go new file mode 100644 index 0000000..bba9af2 --- /dev/null +++ b/fst/id_test.go @@ -0,0 +1,63 @@ +package fst_test + +import ( + "errors" + "testing" + + "git.ophivana.moe/security/fortify/fst" +) + +func TestParseAppID(t *testing.T) { + t.Run("bad length", func(t *testing.T) { + if err := fst.ParseAppID(new(fst.ID), "meow"); !errors.Is(err, fst.ErrInvalidLength) { + t.Errorf("ParseAppID: error = %v, wantErr = %v", err, fst.ErrInvalidLength) + } + }) + + t.Run("bad byte", func(t *testing.T) { + wantErr := "invalid char '\\n' at byte 15" + if err := fst.ParseAppID(new(fst.ID), "02bc7f8936b2af6\n\ne2535cd71ef0bb7"); err == nil || err.Error() != wantErr { + t.Errorf("ParseAppID: error = %v, wantErr = %v", err, wantErr) + } + }) + + t.Run("fuzz 16 iterations", func(t *testing.T) { + for i := 0; i < 16; i++ { + testParseAppIDWithRandom(t) + } + }) +} + +func FuzzParseAppID(f *testing.F) { + for i := 0; i < 16; i++ { + id := new(fst.ID) + if err := fst.NewAppID(id); err != nil { + panic(err.Error()) + } + f.Add(id[0], id[1], id[2], id[3], id[4], id[5], id[6], id[7], id[8], id[9], id[10], id[11], id[12], id[13], id[14], id[15]) + } + + f.Fuzz(func(t *testing.T, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15 byte) { + testParseAppID(t, &fst.ID{b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15}) + }) +} + +func testParseAppIDWithRandom(t *testing.T) { + id := new(fst.ID) + if err := fst.NewAppID(id); err != nil { + t.Fatalf("cannot generate app ID: %v", err) + } + testParseAppID(t, id) +} + +func testParseAppID(t *testing.T, id *fst.ID) { + s := id.String() + got := new(fst.ID) + if err := fst.ParseAppID(got, s); err != nil { + t.Fatalf("cannot parse app ID: %v", err) + } + + if *got != *id { + t.Fatalf("ParseAppID(%#v) = \n%#v, want \n%#v", s, got, id) + } +} diff --git a/fst/shared.go b/fst/shared.go index 3954486..ae59b23 100644 --- a/fst/shared.go +++ b/fst/shared.go @@ -1,18 +1,2 @@ // Package fst exports shared fortify types. package fst - -import ( - "crypto/rand" - "encoding/hex" -) - -type ID [16]byte - -func (a *ID) String() string { - return hex.EncodeToString(a[:]) -} - -func NewAppID(id *ID) error { - _, err := rand.Read(id[:]) - return err -}