144 lines
2.8 KiB
Go
144 lines
2.8 KiB
Go
// 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] + "_***"
|
|
}
|