Files
sendico/api/pkg/db/internal/mongo/verificationimp/hash.go
2026-02-10 01:55:33 +01:00

107 lines
2.3 KiB
Go

package verificationimp
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"github.com/tech/sendico/pkg/model"
)
type TokenHasher interface {
Hash(raw string, token *model.VerificationToken) string
}
type magicHasher struct{}
func (magicHasher) Hash(raw string, _ *model.VerificationToken) string {
return tokenHash(raw)
}
type otpHasher struct{}
func (otpHasher) Hash(raw string, t *model.VerificationToken) string {
return otpHash(raw, *t.Salt)
}
func hasherFor(t *model.VerificationToken) TokenHasher {
if t.Salt != nil {
return otpHasher{}
}
return magicHasher{}
}
const verificationTokenBytes = 32
const otpDigits = 6
func generateMagic() (raw, hash string, err error) {
rawBytes := make([]byte, verificationTokenBytes)
if _, err = rand.Read(rawBytes); err != nil {
return
}
raw = base64.RawURLEncoding.EncodeToString(rawBytes)
hash = tokenHash(raw)
return
}
func generateDeterministicMagic(seed string) (raw, hash string) {
sum := sha256.Sum256([]byte("magic:" + seed))
raw = base64.RawURLEncoding.EncodeToString(sum[:])
hash = tokenHash(raw)
return
}
func generateOTP() (code, salt, hash string, err error) {
// otpDigits-digit code
n := make([]byte, 4)
if _, err = rand.Read(n); err != nil {
return
}
num := int(n[0])<<24 | int(n[1])<<16 | int(n[2])<<8 | int(n[3])
mod := 1
for i := 0; i < otpDigits; i++ {
mod *= 10
}
code = fmt.Sprintf("%0*d", otpDigits, num%mod)
// per-token salt
saltBytes := make([]byte, 16)
if _, err = rand.Read(saltBytes); err != nil {
return
}
salt = base64.RawURLEncoding.EncodeToString(saltBytes)
hash = otpHash(code, salt)
return
}
func generateDeterministicOTP(seed string) (code, salt, hash string) {
sum := sha256.Sum256([]byte("otp:" + seed))
num := int(sum[0])<<24 | int(sum[1])<<16 | int(sum[2])<<8 | int(sum[3])
mod := 1
for i := 0; i < otpDigits; i++ {
mod *= 10
}
code = fmt.Sprintf("%0*d", otpDigits, num%mod)
salt = base64.RawURLEncoding.EncodeToString(sum[4:20])
hash = otpHash(code, salt)
return
}
// We store only the resulting hash (+salt) in DB, never the OTP itself.
func otpHash(code, salt string) string {
sum := sha256.Sum256([]byte(salt + ":" + code))
return hex.EncodeToString(sum[:])
}
func tokenHash(rawToken string) string {
hash := sha256.Sum256([]byte(rawToken))
return base64.RawURLEncoding.EncodeToString(hash[:])
}