diff --git a/itsdangerous.go b/itsdangerous.go index 7d6e50e..94a0790 100644 --- a/itsdangerous.go +++ b/itsdangerous.go @@ -12,9 +12,6 @@ import ( "time" ) -// 2011/01/01 in UTC -const EPOCH = 1293840000 - // Encodes a single string. The resulting string is safe for putting into URLs. func base64Encode(src []byte) string { return base64.RawURLEncoding.EncodeToString(src) @@ -25,8 +22,12 @@ func base64Decode(s string) ([]byte, error) { return base64.RawURLEncoding.DecodeString(s) } +// Function used to obtain the current time. Defaults to time.Now, but can be +// overridden eg for unit tests to simulate a different current time. +var NowFunc = time.Now + // Returns the current timestamp. This implementation returns the -// seconds since 1/1/2011. -func getTimestamp() uint32 { - return uint32(time.Now().Unix() - EPOCH) +// seconds since January 1, 1970 UTC. +func getTimestamp() int64 { + return NowFunc().Unix() } diff --git a/signature.go b/signature.go index 10e8341..268e081 100644 --- a/signature.go +++ b/signature.go @@ -142,13 +142,12 @@ type TimestampSignature struct { // Sign the given string. func (s *TimestampSignature) Sign(value string) (string, error) { - buf := new(bytes.Buffer) + tsBytes := make([]byte, 8) + binary.BigEndian.PutUint64(tsBytes, uint64(getTimestamp())) + // trim leading zeroes + tsBytes = bytes.TrimLeft(tsBytes, "\x00") - if err := binary.Write(buf, binary.BigEndian, getTimestamp()); err != nil { - return "", err - } - - ts := base64Encode(buf.Bytes()) + ts := base64Encode(tsBytes) val := value + s.Sep + ts sig, err := s.Get(val) @@ -160,8 +159,6 @@ func (s *TimestampSignature) Sign(value string) (string, error) { // Unsign the given string. func (s *TimestampSignature) Unsign(value string, maxAge uint32) (string, error) { - var timestamp uint32 - result, err := s.Signature.Unsign(value) if err != nil { return "", err @@ -175,18 +172,22 @@ func (s *TimestampSignature) Unsign(value string, maxAge uint32) (string, error) li := strings.LastIndex(result, s.Sep) val, ts := result[:li], result[li+len(s.Sep):] - sig, err := base64Decode(ts) + tsBytes, err := base64Decode(ts) if err != nil { return "", err } - - buf := bytes.NewReader([]byte(sig)) - if err = binary.Read(buf, binary.BigEndian, ×tamp); err != nil { - return "", err + // left pad up to 8 bytes + if len(tsBytes) < 8 { + tsBytes = append( + make([]byte, 8-len(tsBytes)), + tsBytes..., + ) } + var timestamp = int64(binary.BigEndian.Uint64(tsBytes)) + if maxAge > 0 { - if age := getTimestamp() - timestamp; age > maxAge { + if age := getTimestamp() - timestamp; uint32(age) > maxAge { return "", fmt.Errorf("signature age %d > %d seconds", age, maxAge) } } diff --git a/signature_test.go b/signature_test.go index 026ebb7..5e39044 100644 --- a/signature_test.go +++ b/signature_test.go @@ -1,6 +1,9 @@ package itsdangerous -import "testing" +import ( + "testing" + "time" +) func assert(t *testing.T, actual, expected string) { if actual != expected { @@ -22,9 +25,96 @@ func TestSignatureUnsign(t *testing.T) { assert(t, actual, expected) } +/* +Examples generated in Python as follows: + from freezegun import freeze_time + from itsdangerous import TimestampSigner + with freeze_time("2024-09-27T14:00:00Z"): + s = TimestampSigner("secret_key", "salt") + print(s.sign("my string")) +*/ + +func TestTimestampSignatureSign(t *testing.T) { + tests := []struct { + input string + now time.Time + expected string + }{ + {input: "my string", now: time.Date(2024, 9, 27, 14, 0, 0, 0, time.UTC), + expected: "my string.Zva6YA.aqBNzGvNEDkO6RGFPEX1HIhz0vU"}, + {input: "my string", now: time.Date(2024, 9, 27, 15, 0, 0, 0, time.UTC), + expected: "my string.ZvbIcA.VVQqPkaZ-YQaLHomuudMzTiw45Q"}, + // Test with timestamp > 4 bytes + {input: "my string", now: time.Date(2124, 9, 27, 15, 0, 0, 0, time.UTC), + expected: "my string.ASMOinA.eGqsFVFmYbv8t7tXD8PX7LHSXdY"}, + } + for _, test := range tests { + test := test + t.Run(test.input, func(t *testing.T) { + if !test.now.IsZero() { + NowFunc = func() time.Time { return test.now } + defer func() { NowFunc = time.Now }() + } + + sig := NewTimestampSignature("secret_key", "salt", "", "", nil, nil) + + actual, err := sig.Sign(test.input) + if err != nil { + t.Fatalf("Sign(%s) returned error: %s", test.input, err) + } + if actual != test.expected { + t.Errorf("Sign(%s) got %#v; want %#v", test.input, actual, test.expected) + } + }) + } +} + func TestTimestampSignatureUnsign(t *testing.T) { - s := NewTimestampSignature("secret-key", "", "", "", nil, nil) - expected := "my string" - actual, _ := s.Unsign("my string.BpSAPw.NnKk1nQ206g1c1aJAS1Nxkt4aug", 0) - assert(t, actual, expected) + tests := []struct { + input string + expected string + now time.Time + maxAge uint32 + expectError bool + }{ + // Signature within maxAge + {input: "my string.Zva6YA.aqBNzGvNEDkO6RGFPEX1HIhz0vU", expected: "my string", + now: time.Date(2024, 9, 27, 14, 4, 59, 0, time.UTC), maxAge: 5 * 60}, + // signature expired + {input: "my string.Zva6YA.aqBNzGvNEDkO6RGFPEX1HIhz0vU", expectError: true, + now: time.Date(2024, 9, 27, 14, 5, 1, 0, time.UTC), maxAge: 5 * 60}, + // maxAge zero always validates + {input: "my string.Zva6YA.aqBNzGvNEDkO6RGFPEX1HIhz0vU", expected: "my string", + now: time.Date(2024, 9, 27, 14, 5, 1, 0, time.UTC), maxAge: 0}, + // Test with timestamp > 4 bytes + {input: "my string.ASMOinA.eGqsFVFmYbv8t7tXD8PX7LHSXdY", expected: "my string", + now: time.Date(2124, 9, 27, 15, 4, 59, 0, time.UTC), maxAge: 5 * 60}, + {input: "my string.ASMOinA.eGqsFVFmYbv8t7tXD8PX7LHSXdY", expectError: true, + now: time.Date(2124, 9, 27, 15, 5, 1, 0, time.UTC), maxAge: 5 * 60}, + } + for _, test := range tests { + test := test + t.Run(test.input, func(t *testing.T) { + if !test.now.IsZero() { + NowFunc = func() time.Time { return test.now } + defer func() { NowFunc = time.Now }() + } + + sig := NewTimestampSignature("secret_key", "salt", "", "", nil, nil) + + actual, err := sig.Unsign(test.input, test.maxAge) + if test.expectError { + if err == nil { + t.Fatalf("Unsign(%s) expected error; got no error", test.input) + } + } else { + if err != nil { + t.Fatalf("Unsign(%s) returned error: %s", test.input, err) + } + if actual != test.expected { + t.Errorf("Unsign(%s) got %#v; want %#v", test.input, actual, test.expected) + } + } + }) + } }