diff --git a/pkg/guid/guid.go b/pkg/guid/guid.go index 06ccb97..5864065 100644 --- a/pkg/guid/guid.go +++ b/pkg/guid/guid.go @@ -7,6 +7,7 @@ package guid import ( "crypto/rand" + "crypto/sha1" "encoding" "encoding/binary" "fmt" @@ -59,6 +60,29 @@ func NewV4() (GUID, error) { return g, nil } +// NewV5 returns a new version 5 (generated from a string via SHA-1 hashing) +// GUID, as defined by RFC 4122. The RFC is unclear on the encoding of the name, +// and the sample code treats it as a series of bytes, so we do the same here. +// +// Some implementations, such as those found on Windows, treat the name as a +// big-endian UTF16 stream of bytes. If that is desired, the string can be +// encoded as such before being passed to this function. +func NewV5(namespace GUID, name []byte) (GUID, error) { + b := sha1.New() + namespaceBytes := namespace.ToArray() + b.Write(namespaceBytes[:]) + b.Write(name) + + a := [16]byte{} + copy(a[:], b.Sum(nil)) + + g := FromArray(a) + g.setVersion(5) // Version 5 means generated from a string. + g.setVariant(VariantRFC4122) + + return g, nil +} + func fromArray(b [16]byte, order binary.ByteOrder) GUID { var g GUID g.Data1 = order.Uint32(b[0:4]) diff --git a/pkg/guid/guid_test.go b/pkg/guid/guid_test.go index b84bc41..d7e2124 100644 --- a/pkg/guid/guid_test.go +++ b/pkg/guid/guid_test.go @@ -14,6 +14,14 @@ func mustNewV4(t *testing.T) GUID { return g } +func mustNewV5(t *testing.T, namespace GUID, name []byte) GUID { + g, err := NewV5(namespace, name) + if err != nil { + t.Fatal(err) + } + return g +} + func mustFromString(t *testing.T, s string) GUID { g, err := FromString(s) if err != nil { @@ -112,6 +120,53 @@ func Test_V4HasCorrectVersionAndVariant(t *testing.T) { } } +func Test_V5HasCorrectVersionAndVariant(t *testing.T) { + namespace := mustFromString(t, "f5cbc1a9-4cba-45a0-bfdd-b6761fc7dcc0") + g := mustNewV5(t, namespace, []byte("Foo")) + if g.Version() != 5 { + t.Fatalf("Version is not 5: %s", g) + } + if g.Variant() != VariantRFC4122 { + t.Fatalf("Variant is not RFC4122: %s", g) + } +} + +func Test_V5KnownValues(t *testing.T) { + type testCase struct { + ns GUID + name string + g GUID + } + testCases := []testCase{ + { + mustFromString(t, "6ba7b810-9dad-11d1-80b4-00c04fd430c8"), + "www.sample.com", + mustFromString(t, "4e4463eb-b0e8-54fa-8c28-12d1ab1d45b3"), + }, + { + mustFromString(t, "6ba7b811-9dad-11d1-80b4-00c04fd430c8"), + "https://www.sample.com/test", + mustFromString(t, "9e44625a-0d85-5e0a-99bc-8e8a77df5ea2"), + }, + { + mustFromString(t, "6ba7b812-9dad-11d1-80b4-00c04fd430c8"), + "1.3.6.1.4.1.343", + mustFromString(t, "6aab0456-7392-582a-b92a-ba5a7096945d"), + }, + { + mustFromString(t, "6ba7b814-9dad-11d1-80b4-00c04fd430c8"), + "CN=John Smith, ou=People, o=FakeCorp, L=Seattle, S=Washington, C=US", + mustFromString(t, "badff8dd-c869-5b64-a260-00092e66be00"), + }, + } + for _, tc := range testCases { + g := mustNewV5(t, tc.ns, []byte(tc.name)) + if g != tc.g { + t.Fatalf("GUIDs are not equal.\nExpected: %s\nActual: %s", tc.g, g) + } + } +} + func Test_ToArray(t *testing.T) { g := mustFromString(t, "73c39589-192e-4c64-9acf-6c5d0aa18528") b := g.ToArray()