Option 1: md5 hash the bcrypt output
Props to OP for mostly answering his own question :)
I think it'll satisfy everything you're looking for (32-character length):
package main
import (
"crypto/md5"
"encoding/hex"
"fmt"
"log"
"golang.org/x/crypto/bcrypt"
)
// GenerateToken returns a unique token based on the provided email string
func GenerateToken(email string) string {
hash, err := bcrypt.GenerateFromPassword([]byte(email), bcrypt.DefaultCost)
if err != nil {
log.Fatal(err)
}
fmt.Println("Hash to store:", string(hash))
hasher := md5.New()
hasher.Write(hash)
return hex.EncodeToString(hasher.Sum(nil))
}
func main() {
fmt.Println("token:", GenerateToken("bob@webserver.com"))
}
$ go run main.go
Hash to store: $2a$10$B23cv7lDpbY3iVvfZ7GYE.e4691ow8i7l6CQXkmz315fbg4jLzoue
token: 90a514ab93e2c32fdd1072154b26a100
Below are 2 of my previous suggestions that I doubt will work better for you, but could be helpful for others to consider.
Option 2: base64
In the past, I've used base64 encoding to make tokens more portable after encryption/hashing. Here's a working example:
package main
import (
"encoding/base64"
"fmt"
"log"
"golang.org/x/crypto/bcrypt"
)
// GenerateToken returns a unique token based on the provided email string
func GenerateToken(email string) string {
hash, err := bcrypt.GenerateFromPassword([]byte(email), bcrypt.DefaultCost)
if err != nil {
log.Fatal(err)
}
fmt.Println("Hash to store:", string(hash))
return base64.StdEncoding.EncodeToString(hash)
}
func main() {
fmt.Println("token:", GenerateToken("bob@webserver.com"))
}
$ go run main.go
Hash to store: $2a$10$cbVMU9U665VSqpfwrNZWOeU5cIDOe5iBJ8ZVa2yJCTsnk9MEZHvRq
token: JDJhJDEwJGNiVk1VOVU2NjVWU3FwZndyTlpXT2VVNWNJRE9lNWlCSjhaVmEyeUpDVHNuazlNRVpIdlJx
As you can see, this unfortunately doesn't provide you with a 16-32 character length. If you're okay with the length being 80 long, then this might work for you.
Option 3: url path/query escapes
I also tried url.PathEscape and url.QueryEscape to be thorough. While they have the same problem as the base64 example (length, though a bit shorter), at least they "should" work in the path:
// GenerateToken returns a unique token based on the provided email string
func GenerateToken(email string) string {
hash, err := bcrypt.GenerateFromPassword([]byte(email), bcrypt.DefaultCost)
if err != nil {
log.Fatal(err)
}
fmt.Println("Hash to store:", string(hash))
fmt.Println("url.PathEscape:", url.PathEscape(string(hash)))
fmt.Println("url.QueryEscape:", url.QueryEscape(string(hash)))
return base64.StdEncoding.EncodeToString(hash)
}
url.PathEscape: $2a$10$wx1UL6%2F7RD6sFq7Bzpgcc.ibFSW114Tf6A521wRh9rVy8dp%2Fa82x2
url.QueryEscape: %242a%2410%24wx1UL6%2F7RD6sFq7Bzpgcc.ibFSW114Tf6A521wRh9rVy8dp%2Fa82x2