// Key structure: hmy_live_<43 secret><6 checksum> package apikey import ( "crypto/rand" "crypto/sha256" "encoding/base64" "errors" "fmt" "hash/crc32" "strings" ) const ( Issuer = "hmy" secretBytes = 32 // 256 bit secretLen = 43 checksumLen = 6 bodyLen = secretLen + checksumLen // Enough to identify, not enough to brute-force displayLen = 16 ) var ( ErrMalformed = errors.New("apikey: malformed key") ErrBadChecksum = errors.New("apikey: checksum mismatch") ErrEnvironment = errors.New("apikey: unknown environment") ) // Environment for live and test keys type Environment string const ( Live Environment = "live" Test Environment = "test" ) func (e Environment) valid() bool { return e == Live || e == Test } // Generated is the result of issuing a key. // Plaintext shown to owner and never stored. type Generated struct { Plaintext string Hash []byte Display string Env Environment } // Castagnoli instead of IEEE table var crcTable = crc32.MakeTable(crc32.Castagnoli) func checksum(secret string) string { sum := crc32.Checksum([]byte(secret), crcTable) b := []byte{ byte(sum >> 24), byte(sum >> 16), byte(sum >> 8), byte(sum), } return base64.RawURLEncoding.EncodeToString(b) } // Generate issues a ney key. Store Hash and Display, show Plaintext once func Generate(env Environment) (Generated, error) { if !env.valid() { return Generated{}, ErrEnvironment } buf := make([]byte, secretBytes) if _, err := rand.Read(buf); err != nil { return Generated{}, fmt.Errorf("apikey: reading entropy: %w", err) } secret := base64.RawURLEncoding.EncodeToString(buf) plaintext := Issuer + "_" + string(env) + "_" + secret + checksum(secret) display := plaintext if len(display) > displayLen { display = display[:displayLen] } return Generated{ Plaintext: plaintext, Hash: Hash(plaintext), Display: display, Env: env, }, nil } // Hash is what is stored and looked up func Hash(key string) []byte { sum := sha256.Sum256([]byte(key)) return sum[:] } // Validate checks the structure and checksum. Call before hashing. func Validate(key string) (Environment, error) { parts := strings.SplitN(key, "_", 3) if len(parts) != 3 { return "", ErrMalformed } if parts[0] != Issuer { return "", ErrMalformed } env := Environment(parts[1]) if !env.valid() { return "", ErrEnvironment } body := parts[2] if len(body) != bodyLen { return "", ErrMalformed } secret, want := body[:secretLen], body[secretLen:] if _, err := base64.RawURLEncoding.DecodeString(secret); err != nil { return "", ErrMalformed } if checksum(secret) != want { return "", ErrBadChecksum } return env, nil } // Redact makes the key safe for logging func Redact(key string) string { if len(key) <= displayLen { return Issuer + "_***" } return key[:displayLen] + "_***" }