├── go.mod ├── LICENSE ├── .github └── workflows │ └── test.yml ├── danger_test.go ├── README.md └── danger.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pyr/go-itsdangerous 2 | 3 | go 1.25.0 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 Pierre-Yves Ritschard 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Go CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ "**" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | concurrency: 13 | group: go-ci-${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | test: 18 | runs-on: ubuntu-latest 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | go-version: [ '1.24.x', '1.25.x' ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Set up Go 30 | uses: actions/setup-go@v5 31 | with: 32 | go-version: ${{ matrix.go-version }} 33 | check-latest: true 34 | cache: true # caches module & build cache based on go.sum 35 | 36 | - name: Print Go version 37 | run: go version 38 | 39 | - name: Static analysis (vet) 40 | run: go vet ./... 41 | 42 | - name: Run tests (race, coverage) 43 | run: | 44 | mkdir -p coverage 45 | go test ./... \ 46 | -race \ 47 | -covermode=atomic \ 48 | -coverprofile=coverage/coverage.out \ 49 | -shuffle=on \ 50 | -v 51 | 52 | - name: Upload coverage artifact 53 | uses: actions/upload-artifact@v4 54 | if: always() 55 | with: 56 | name: coverage-${{ matrix.go-version }} 57 | path: coverage/coverage.out 58 | if-no-files-found: warn 59 | -------------------------------------------------------------------------------- /danger_test.go: -------------------------------------------------------------------------------- 1 | package itsdangerous 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestSignVerifyStringRound(t *testing.T) { 11 | clock := &fakeClock{timestamp: 1234} 12 | s := NewSigner("foobarbimbaz", "application") 13 | s.WithClock(clock) 14 | 15 | payloads := []string{"hello", "foobar", "something else"} 16 | 17 | for _, p := range payloads { 18 | t.Run(fmt.Sprintf("can do a roundtrip signature for: %s", p), 19 | func(t *testing.T) { 20 | signed := s.SignString(p) 21 | decoded, timestamp, err := s.VerifyString(signed) 22 | 23 | if err != nil { 24 | t.Fatalf("roundtrip failed for payload %s: %v", p, err) 25 | 26 | } 27 | 28 | if timestamp != 1234 { 29 | t.Fatalf("invalid timestamp found in signature: %d", timestamp) 30 | } 31 | 32 | if decoded != p { 33 | t.Fatalf("could not find original payload in signed string: %s vs. %s", decoded, p) 34 | } 35 | }) 36 | } 37 | } 38 | 39 | func TestSignVerifyStringRoundWithOldKeys(t *testing.T) { 40 | clock := &fakeClock{timestamp: 1234} 41 | s1 := NewSigner("foobarbimbaz", "application").WithClock(clock) 42 | 43 | s2 := NewSigner("helloiamadifferentkey", "application").WithClock(clock).WithExtraKey("foobarbimbaz") 44 | 45 | payloads := []string{"hello", "foobar", "something else"} 46 | 47 | for _, p := range payloads { 48 | t.Run(fmt.Sprintf("can do a roundtrip signature for: %s", p), 49 | func(t *testing.T) { 50 | signed := s1.SignString(p) 51 | decoded, timestamp, err := s2.VerifyString(signed) 52 | 53 | if err != nil { 54 | t.Fatalf("roundtrip failed for payload %s: %v", p, err) 55 | 56 | } 57 | 58 | if timestamp != 1234 { 59 | t.Fatalf("invalid timestamp found in signature: %d", timestamp) 60 | } 61 | 62 | if decoded != p { 63 | t.Fatalf("could not find original payload in signed string: %s vs. %s", decoded, p) 64 | } 65 | }) 66 | } 67 | } 68 | 69 | func TestSignVerifyRound(t *testing.T) { 70 | clock := &fakeClock{timestamp: 1234} 71 | s := NewSigner("foobarbimbaz", "application") 72 | s.WithClock(clock) 73 | 74 | payloads := []Payload{ 75 | {Identifier: "root", Expiry: "1s", Role: "admin"}, 76 | {Identifier: "u1", Expiry: "1s", Role: "user"}, 77 | } 78 | 79 | for _, p := range payloads { 80 | t.Run(fmt.Sprintf("can do a roundtrip signature on behalf of: %s", p.Identifier), 81 | func(t *testing.T) { 82 | signed, err := s.Sign(p) 83 | if err != nil { 84 | t.Fatalf("could not sign: %v", err) 85 | 86 | } 87 | decoded, err := s.Verify(signed) 88 | if err != nil { 89 | t.Fatalf("could not verify: %v", err) 90 | } 91 | 92 | if !reflect.DeepEqual(decoded, &p) { 93 | t.Fatalf("payload contents have changed") 94 | } 95 | }) 96 | } 97 | } 98 | 99 | func TestVerifyExpiryRound(t *testing.T) { 100 | clock := &fakeClock{timestamp: 1234} 101 | s := NewSigner("foobarbimbaz", "application") 102 | s.WithClock(clock) 103 | 104 | payloads := []Payload{ 105 | {Identifier: "root", Expiry: "1s", Role: "admin"}, 106 | {Identifier: "u1", Expiry: "1s", Role: "user"}, 107 | } 108 | 109 | for _, p := range payloads { 110 | t.Run(fmt.Sprintf("roundtrip with expiry is honored on behalf of: %s", p.Identifier), 111 | func(t *testing.T) { 112 | signed, err := s.Sign(p) 113 | if err != nil { 114 | t.Fatalf("could not sign: %v", err) 115 | 116 | } 117 | clock.shiftEpoch(5) 118 | decoded, err := s.Verify(signed) 119 | if !errors.Is(err, ErrSignatureExpired) { 120 | t.Fatalf("verification should have failed due to expiry") 121 | } 122 | 123 | if !reflect.DeepEqual(decoded, &p) { 124 | t.Fatalf("payload contents have changed") 125 | } 126 | }) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-itsdangerous 2 | 3 | A Go implementation of the ItsDangerous signed token library, compatible with Python's [itsdangerous](https://itsdangerous.palletsprojects.com/en/stable/) library. 4 | 5 | ## Overview 6 | 7 | This package provides cryptographically signed tokens for securely transmitting data. It uses HMAC-based signatures to ensure data integrity and authenticity, making it ideal for session cookies, API tokens, and other use cases where you need to verify that data hasn't been tampered with. 8 | 9 | **Important**: This library provides data integrity and authenticity, but does not encrypt data. Never store confidential information in signed payloads as they can be read by anyone. 10 | 11 | ## Features 12 | 13 | - HMAC-based token signing and verification 14 | - Support for token expiration 15 | - Key rotation support (multiple verification keys) 16 | - Configurable salt for namespacing 17 | - Pluggable hash algorithms (SHA-512 by default) 18 | - Compatible with Python's itsdangerous library 19 | 20 | ## Installation 21 | 22 | ```bash 23 | go get github.com/pyr/go-itsdangerous 24 | ``` 25 | 26 | ## Usage 27 | 28 | ### Basic String Signing 29 | 30 | ```go 31 | package main 32 | 33 | import ( 34 | "fmt" 35 | "github.com/pyr/go-itsdangerous" 36 | ) 37 | 38 | func main() { 39 | // Create a signer with a secret key and salt 40 | signer := itsdangerous.NewSigner("your-secret-key", "your-app-name") 41 | 42 | // Sign a string 43 | signed := signer.SignString("hello world") 44 | fmt.Println("Signed:", signed) 45 | 46 | // Verify the signed string 47 | payload, timestamp, err := signer.VerifyString(signed) 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | fmt.Println("Payload:", payload) 53 | fmt.Println("Timestamp:", timestamp) 54 | } 55 | ``` 56 | 57 | ### Structured Payload Signing 58 | 59 | ```go 60 | // Create a payload with expiration 61 | payload := itsdangerous.Payload{ 62 | Identifier: "user123", 63 | Role: "admin", 64 | Expiry: "1h", // Expires in 1 hour 65 | Data: map[string]string{ 66 | "session_id": "abc123", 67 | }, 68 | } 69 | 70 | // Sign the payload 71 | signed, err := signer.Sign(payload) 72 | if err != nil { 73 | panic(err) 74 | } 75 | 76 | // Verify and decode the payload 77 | decoded, err := signer.Verify(signed) 78 | if err != nil { 79 | panic(err) 80 | } 81 | 82 | fmt.Printf("User: %s, Role: %s\n", decoded.Identifier, decoded.Role) 83 | ``` 84 | 85 | ### Key Rotation 86 | 87 | ```go 88 | // Create signer with primary key 89 | signer := itsdangerous.NewSigner("new-secret-key", "app-name") 90 | 91 | // Add old keys for verification (allows gradual key rotation) 92 | signer = signer.WithExtraKey("old-secret-key-1", "old-secret-key-2") 93 | 94 | // Tokens signed with any of these keys will be valid for verification 95 | // New tokens will be signed with the primary key only 96 | ``` 97 | 98 | ### Custom Configuration 99 | 100 | ```go 101 | import "crypto/sha256" 102 | 103 | signer := itsdangerous.NewSigner("secret", "salt"). 104 | WithHasher(sha256.New). // Use SHA-256 instead of SHA-512 105 | WithSalt("custom-namespace") 106 | ``` 107 | 108 | ## API Reference 109 | 110 | ### Signer 111 | 112 | - `NewSigner(primaryKey, salt string) *Signer` - Create a new signer 113 | - `SignString(payload string) string` - Sign a string 114 | - `VerifyString(signed string) (string, int64, error)` - Verify a signed string 115 | - `Sign(payload Payload) (string, error)` - Sign a structured payload 116 | - `Verify(signed string) (*Payload, error)` - Verify and decode a structured payload 117 | 118 | ### Configuration Methods 119 | 120 | - `WithExtraKey(keys ...string) *Signer` - Add additional verification keys 121 | - `WithSalt(salt string) *Signer` - Set custom salt 122 | - `WithHasher(hasher func() hash.Hash) *Signer` - Set custom hash algorithm 123 | - `WithClock(clock Clock) *Signer` - Set custom clock (mainly for testing) 124 | 125 | ### Payload Structure 126 | 127 | ```go 128 | type Payload struct { 129 | Expiry string `json:"expiry,omitempty"` // Duration string (e.g., "1h", "30m") 130 | Identifier string `json:"identifier,omitempty"` // User/entity identifier 131 | Role string `json:"role,omitempty"` // Role or permission level 132 | Data map[string]string `json:"data,omitempty"` // Additional key-value data 133 | } 134 | ``` 135 | 136 | ## Security Considerations 137 | 138 | 1. **Keep secret keys secure** - Store them in environment variables or secure configuration 139 | 2. **Use strong, random keys** - Generate cryptographically secure random keys 140 | 3. **Rotate keys regularly** - Use the key rotation feature to gradually replace old keys 141 | 4. **Don't store secrets in payloads** - Signed data is not encrypted and can be read by anyone 142 | 5. **Use appropriate expiration times** - Set reasonable expiry durations for your use case 143 | 144 | ## Testing 145 | 146 | ```bash 147 | go test 148 | ``` 149 | 150 | ## License 151 | 152 | ISC licensed, see [LICENSE](LICENSE). 153 | -------------------------------------------------------------------------------- /danger.go: -------------------------------------------------------------------------------- 1 | // ItsDangerous signed token implementation. 2 | // This package provides a new type Signer which offers both `SignString` and `VerifyString` 3 | // 4 | // Signer hold configuration for token signing and verifying. This follows 5 | // the scheme documented here: https://itsdangerous.palletsprojects.com/en/stable/ 6 | // 7 | // ItsDangerous uses a simple hmac-based scheme to sign arbitrary strings, in the 8 | // Python world, this scheme is often used to sign cookies. Note that ItsDangerous 9 | // provides no concrete way to hide data from onlookers, but simply to sign 10 | // payloads, it is thus important never to store confidential data in the 11 | // signed payload. 12 | // 13 | // ItsDangerous relies on knowledge that needs to be shared out-of-band between 14 | // signers and verifiers: 15 | // 16 | // - A secret key for signing 17 | // - A set of acceptable secret keys for verifying, allowing for rolling keys 18 | // - A somewhat badly named salt, used for namespacing 19 | // - A hashing algorithm, SHA-512 is used by default 20 | // 21 | // To simplify the classical use of using this for cookie signing of sessions, 22 | // A `Sign` and `Verify` signatures are provided which operate on a payload 23 | // structure supporting expiry. the Expiry is expressed as a valid go duration 24 | // expression. 25 | package itsdangerous 26 | 27 | import ( 28 | "crypto/hmac" 29 | "crypto/sha512" 30 | "encoding/base64" 31 | "encoding/binary" 32 | "encoding/json" 33 | "fmt" 34 | "hash" 35 | "strings" 36 | "time" 37 | ) 38 | 39 | // Clock provides an interface for obtaining the current time as a Unix timestamp. 40 | // This abstraction allows for deterministic testing by injecting a fake clock. 41 | type Clock interface { 42 | // Epoch returns the current time as a Unix timestamp (seconds since January 1, 1970 UTC). 43 | Epoch() int64 44 | } 45 | 46 | // Signer holds configuration for token signing and verification operations. 47 | // It contains the cryptographic keys, salt for namespacing, clock for timestamps, 48 | // and hash function used for HMAC operations. 49 | type Signer struct { 50 | keys []string 51 | salt string 52 | clock Clock 53 | hasher func() hash.Hash 54 | } 55 | 56 | type wallClock struct{} 57 | 58 | type fakeClock struct { 59 | timestamp int64 60 | } 61 | 62 | // Payload represents a structured data container that can be signed and verified. 63 | // It supports expiration times, user identification, role-based access, and arbitrary data. 64 | type Payload struct { 65 | // Expiry specifies when the payload expires as a Go duration string (e.g., "1h", "30m", "24h"). 66 | // If empty, the payload never expires. 67 | Expiry string `json:"expiry,omitempty"` 68 | 69 | // Identifier is typically used to store a user ID or entity identifier. 70 | Identifier string `json:"identifier,omitempty"` 71 | 72 | // Role represents the permission level or role associated with this payload. 73 | Role string `json:"role,omitempty"` 74 | 75 | // Data contains arbitrary key-value pairs for additional payload information. 76 | Data map[string]string `json:"data,omitempty"` 77 | } 78 | 79 | // WallClock is the default clock implementation that returns the current system time. 80 | // It is used by default when creating new Signer instances. 81 | var WallClock = wallClock{} 82 | 83 | // SignatureExpiredError is returned when a payload expiry has been reached at verification time. 84 | var ErrSignatureExpired = fmt.Errorf("payload signature has expired") 85 | 86 | func (wallClock) Epoch() int64 { 87 | return time.Now().Unix() 88 | } 89 | 90 | func (fake *fakeClock) Epoch() int64 { 91 | return fake.timestamp 92 | } 93 | 94 | func (fake *fakeClock) shiftEpoch(delta int64) { 95 | fake.timestamp += delta 96 | } 97 | 98 | func (s *Signer) primaryKey() string { 99 | return s.keys[0] 100 | } 101 | 102 | func (s *Signer) stringToSign(payload string) string { 103 | clockBytes := make([]byte, 8) 104 | binary.LittleEndian.PutUint64(clockBytes, uint64(s.clock.Epoch())) 105 | 106 | return fmt.Sprintf("%s.%s", 107 | base64.URLEncoding.EncodeToString([]byte(payload)), 108 | base64.URLEncoding.EncodeToString([]byte(clockBytes))) 109 | } 110 | 111 | func (s *Signer) createKeyMac(key string) hash.Hash { 112 | mac := hmac.New(s.hasher, []byte(key)) 113 | derivedKey := mac.Sum([]byte(s.salt)) 114 | derivedMac := hmac.New(s.hasher, derivedKey) 115 | return derivedMac 116 | } 117 | 118 | func (s *Signer) createSignature(input []byte) string { 119 | 120 | mac := s.createKeyMac(s.primaryKey()) 121 | output := mac.Sum([]byte(input)) 122 | return base64.URLEncoding.EncodeToString(output) 123 | } 124 | 125 | // Sign creates a cryptographically signed token from a structured Payload. 126 | // The payload is JSON-encoded and then signed using the signer's primary key. 127 | // If the payload contains an Expiry field, it must be a valid Go duration string. 128 | // 129 | // Returns the signed token string or an error if the payload cannot be marshaled 130 | // or contains an invalid expiry duration. 131 | func (s *Signer) Sign(payload Payload) (string, error) { 132 | // Avoid late errors due to invalid durations 133 | if payload.Expiry != "" { 134 | _, err := time.ParseDuration(payload.Expiry) 135 | if err != nil { 136 | return "", err 137 | } 138 | } 139 | bs, err := json.Marshal(payload) 140 | if err != nil { 141 | return "", err 142 | } 143 | return s.SignString(string(bs)), nil 144 | } 145 | 146 | // Verify validates a signed token and returns the decoded Payload. 147 | // It first verifies the cryptographic signature using any of the configured keys, 148 | // then checks if the payload has expired based on its Expiry field and signing timestamp. 149 | // 150 | // Returns the decoded payload or an error if the signature is invalid, 151 | // the payload is malformed, or the token has expired. 152 | func (s *Signer) Verify(signedString string) (*Payload, error) { 153 | decoded, timestamp, err := s.VerifyString(signedString) 154 | if err != nil { 155 | return nil, err 156 | } 157 | var p Payload 158 | err = json.Unmarshal([]byte(decoded), &p) 159 | if err != nil { 160 | return nil, err 161 | } 162 | if p.Expiry != "" { 163 | tm := time.Unix(timestamp, 0) 164 | now := time.Unix(s.clock.Epoch(), 0) 165 | duration, err := time.ParseDuration(p.Expiry) 166 | if err != nil { 167 | return &p, err 168 | } 169 | if tm.Add(duration).Before(now) { 170 | return &p, ErrSignatureExpired 171 | } 172 | } 173 | return &p, nil 174 | } 175 | 176 | // SignString creates a cryptographically signed token from a raw string payload. 177 | // The resulting signed string contains three dot-separated parts: 178 | // 1. Base64-encoded payload 179 | // 2. Base64-encoded timestamp 180 | // 3. Base64-encoded HMAC signature 181 | // 182 | // This is the low-level signing method used by Sign(). 183 | func (s *Signer) SignString(payload string) string { 184 | input := s.stringToSign(payload) 185 | sig := s.createSignature([]byte(input)) 186 | return fmt.Sprintf("%s.%s", input, sig) 187 | } 188 | 189 | func (s *Signer) verifySignature(stringToSign string, signature string) error { 190 | decodedSig, err := base64.URLEncoding.DecodeString(signature) 191 | if err != nil { 192 | return err 193 | } 194 | for _, k := range s.keys { 195 | mac := s.createKeyMac(k) 196 | output := mac.Sum([]byte(stringToSign)) 197 | if hmac.Equal(decodedSig, output) { 198 | return nil 199 | } 200 | } 201 | return fmt.Errorf("no key permitted to validate signature") 202 | } 203 | 204 | // VerifyString validates the cryptographic signature of a signed token and extracts its contents. 205 | // It parses the three-part token format, decodes the payload and timestamp, 206 | // and verifies the signature against all configured keys. 207 | // 208 | // Returns the decoded payload string, the signing timestamp as Unix seconds, 209 | // and an error if the token format is invalid or signature verification fails. 210 | func (s *Signer) VerifyString(input string) (string, int64, error) { 211 | 212 | parts := strings.SplitN(input, ".", 3) 213 | if len(parts) != 3 { 214 | return "", 0, fmt.Errorf("invalid payload format") 215 | } 216 | payload, err := base64.URLEncoding.DecodeString(parts[0]) 217 | if err != nil { 218 | return "", 0, err 219 | } 220 | clockBytes, err := base64.URLEncoding.DecodeString(parts[1]) 221 | if err != nil { 222 | return string(payload), 0, err 223 | } 224 | timestamp := int64(binary.LittleEndian.Uint64(clockBytes)) 225 | 226 | err = s.verifySignature(strings.Join(parts[:2], "."), parts[2]) 227 | return string(payload), timestamp, err 228 | 229 | } 230 | 231 | // NewSigner creates a new Signer instance with the specified primary key and salt. 232 | // The signer is configured with default settings: 233 | // - Uses the provided primaryKey for signing (additional keys can be added for verification) 234 | // - Uses the provided salt for key derivation and namespacing 235 | // - Uses WallClock for timestamps 236 | // - Uses SHA-512 as the hash algorithm 237 | // 238 | // The salt should be unique per application to prevent signature reuse across different contexts. 239 | func NewSigner(primaryKey string, salt string) *Signer { 240 | return &Signer{ 241 | keys: []string{primaryKey}, 242 | salt: salt, 243 | clock: WallClock, 244 | hasher: sha512.New, 245 | } 246 | } 247 | 248 | // WithClock configures the signer to use a custom clock for timestamp generation. 249 | // This is primarily useful for testing with deterministic timestamps. 250 | // Returns the signer instance for method chaining. 251 | func (s *Signer) WithClock(clock Clock) *Signer { 252 | s.clock = clock 253 | return s 254 | } 255 | 256 | // WithExtraKey adds additional keys that can be used for signature verification. 257 | // This enables key rotation: new tokens are signed with the primary key, 258 | // but tokens signed with any of the extra keys will still verify successfully. 259 | // Returns the signer instance for method chaining. 260 | func (s *Signer) WithExtraKey(keys ...string) *Signer { 261 | return s.WithExtraKeys(keys) 262 | } 263 | 264 | // WithExtraKeys adds a slice of additional keys that can be used for signature verification. 265 | // This enables key rotation: new tokens are signed with the primary key, 266 | // but tokens signed with any of the extra keys will still verify successfully. 267 | // Returns the signer instance for method chaining. 268 | func (s *Signer) WithExtraKeys(keys []string) *Signer { 269 | s.keys = append(s.keys, keys...) 270 | return s 271 | } 272 | 273 | // WithSalt configures the signer to use a custom salt for key derivation. 274 | // The salt provides namespacing to prevent signature reuse across different applications 275 | // or contexts. Changing the salt will invalidate all existing signatures. 276 | // Returns the signer instance for method chaining. 277 | func (s *Signer) WithSalt(salt string) *Signer { 278 | s.salt = salt 279 | return s 280 | } 281 | 282 | // WithHasher configures the signer to use a custom hash algorithm for HMAC operations. 283 | // The default is SHA-512. Common alternatives include sha256.New for SHA-256. 284 | // Changing the hasher will invalidate all existing signatures. 285 | // Returns the signer instance for method chaining. 286 | func (s *Signer) WithHasher(hasher func() hash.Hash) *Signer { 287 | s.hasher = hasher 288 | return s 289 | } 290 | --------------------------------------------------------------------------------