diff --git a/mcrypto/mcrypto.go b/mcrypto/mcrypto.go new file mode 100644 index 0000000..3e64660 --- /dev/null +++ b/mcrypto/mcrypto.go @@ -0,0 +1,4 @@ +// Package mcrypto contains general purpose functionality related to +// cryptography, notably related to unique identifiers, signing/verifying data, +// and encrypting/decrypting data +package mcrypto diff --git a/mcrypto/uuid.go b/mcrypto/uuid.go new file mode 100644 index 0000000..58dd7a3 --- /dev/null +++ b/mcrypto/uuid.go @@ -0,0 +1,103 @@ +package mcrypto + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/mediocregopher/mediocre-go-lib/mlog" +) + +const ( + uuidV0Prefix = "0u" + uuidV0Len = 34 // prefix:2 + hexEncodedLen(time:8 + random:8) +) + +// UUID is a universally unique identifier which embeds within it a timestamp. +// +// Only Unmarshal methods should be called on the zero UUID value. +// +// Comparing the equality of two UUID's should always be done using the Equal +// method, or by comparing their string forms. +// +// The string form of UUIDs (returned by String or MarshalText) are +// lexigraphically order-able by their embedded timestamp. +type UUID struct { + // the UUID type is actually just an opaque wrapper. For the most part + // UUID's don't ever need the information in them (like their timestamp) + // unpacked, so it's more efficient to just keep the string and unpack + // on-the-fly + str string +} + +// NewUUID populates and returns a new UUID instance which embeds the given time +func NewUUID(t time.Time) UUID { + b := make([]byte, 16) + binary.BigEndian.PutUint64(b[:8], uint64(t.UnixNano())) + if _, err := rand.Read(b[8:]); err != nil { + panic(err) + } + return UUID{ + str: uuidV0Prefix + hex.EncodeToString(b), + } +} + +func (u UUID) String() string { + return u.str +} + +// Equal returns whether or not the two UUID's are the same value +func (u UUID) Equal(u2 UUID) bool { + return u.str == u2.str +} + +// Time unpacks and returns the timestamp embedded in the UUID +func (u UUID) Time() time.Time { + b, err := hex.DecodeString(u.str[2:]) + if err != nil { + // once a UUID has been created it should always be valid + panic(fmt.Sprintf("malformed UUID: %q", u.str)) + } + unixNano := int64(binary.BigEndian.Uint64(b[:8])) + return time.Unix(0, unixNano).Local() +} + +// KV implements the method for the mlog.KVer interface +func (u UUID) KV() mlog.KV { + return mlog.KV{"uuid": u.String()} +} + +// MarshalText implements the method for the encoding.TextMarshaler interface +func (u UUID) MarshalText() ([]byte, error) { + return []byte(u.String()), nil +} + +// UnmarshalText implements the method for the encoding.TextUnmarshaler +// interface +func (u *UUID) UnmarshalText(b []byte) error { + if !bytes.HasPrefix(b, []byte(uuidV0Prefix)) || len(b) != uuidV0Len { + err := errors.New("malformed uuid string") + return mlog.ErrWithKV(err, mlog.KV{"uuidStr": string(b)}) + } + u.str = string(b) + return nil +} + +// MarshalJSON implements the method for the json.Marshaler interface +func (u UUID) MarshalJSON() ([]byte, error) { + return json.Marshal(u.String()) +} + +// UnmarshalJSON implements the method for the json.Unmarshaler interface +func (u *UUID) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + return u.UnmarshalText([]byte(s)) +} diff --git a/mcrypto/uuid_test.go b/mcrypto/uuid_test.go new file mode 100644 index 0000000..00a0bbb --- /dev/null +++ b/mcrypto/uuid_test.go @@ -0,0 +1,40 @@ +package mcrypto + +import ( + "strings" + . "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUUID(t *T) { + var prevT time.Time + var prev UUID + for i := 0; i < 10000; i++ { + thisT := time.Now().Round(0) // strip monotonic clock + require.True(t, thisT.After(prevT)) // sanity check + this := NewUUID(thisT) + + // basic + assert.True(t, strings.HasPrefix(this.String(), uuidV0Prefix)) + assert.Len(t, this.String(), uuidV0Len) + + // comparisons with prev + assert.False(t, prev.Equal(this)) + assert.NotEqual(t, prev.String(), this.String()) + assert.True(t, this.String() > prev.String()) + prev = this + + // check time unpacking + assert.Equal(t, thisT, this.Time()) + + // check marshal/unmarshal + thisStr, err := this.MarshalText() + require.NoError(t, err) + var this2 UUID + require.NoError(t, this2.UnmarshalText(thisStr)) + assert.True(t, this.Equal(this2)) + } +}