| 1 | package crypto |
| 2 | |
| 3 | import ( |
| 4 | "crypto/hmac" |
| 5 | "crypto/sha256" |
| 6 | "fmt" |
| 7 | "strings" |
| 8 | |
| 9 | "golang.org/x/crypto/argon2" |
| 10 | "golang.org/x/crypto/pbkdf2" |
| 11 | ) |
| 12 | |
| 13 | // KdfType represents the key derivation function type. |
| 14 | type KdfType int |
| 15 | |
| 16 | const ( |
| 17 | KdfPBKDF2 KdfType = 0 |
| 18 | KdfArgon2id KdfType = 1 |
| 19 | ) |
| 20 | |
| 21 | // DeriveKeyConfig holds KDF parameters returned by prelogin. |
| 22 | type DeriveKeyConfig struct { |
| 23 | KdfType KdfType |
| 24 | KdfIterations int |
| 25 | KdfMemory int // Argon2id only, in MB |
| 26 | KdfParallelism int // Argon2id only |
| 27 | } |
| 28 | |
| 29 | // DeriveMasterKey derives the master key from the master password and email. |
| 30 | func DeriveMasterKey(password, email string, cfg DeriveKeyConfig) ([]byte, error) { |
| 31 | salt := []byte(strings.ToLower(email)) |
| 32 | |
| 33 | switch cfg.KdfType { |
| 34 | case KdfPBKDF2: |
| 35 | return pbkdf2.Key([]byte(password), salt, cfg.KdfIterations, 32, sha256.New), nil |
| 36 | case KdfArgon2id: |
| 37 | if cfg.KdfMemory <= 0 || cfg.KdfParallelism <= 0 || cfg.KdfIterations <= 0 { |
| 38 | return nil, fmt.Errorf("invalid argon2id parameters: memory=%d parallelism=%d iterations=%d", |
| 39 | cfg.KdfMemory, cfg.KdfParallelism, cfg.KdfIterations) |
| 40 | } |
| 41 | // Argon2id uses a SHA256 hash of the email as salt |
| 42 | saltHash := sha256.Sum256(salt) |
| 43 | return argon2.IDKey([]byte(password), saltHash[:], uint32(cfg.KdfIterations), uint32(cfg.KdfMemory*1024), uint8(cfg.KdfParallelism), 32), nil |
| 44 | default: |
| 45 | return nil, fmt.Errorf("unsupported KDF type: %d", cfg.KdfType) |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | // HashMasterPassword hashes the master key with the password for server auth. |
| 50 | // This is what gets sent to the server as the "master password hash". |
| 51 | func HashMasterPassword(masterKey []byte, password string) []byte { |
| 52 | return pbkdf2.Key(masterKey, []byte(password), 1, 32, sha256.New) |
| 53 | } |
| 54 | |
| 55 | // StretchMasterKey expands the 32-byte master key to 64 bytes (32 enc + 32 mac) |
| 56 | // using HKDF-expand with SHA256. |
| 57 | func StretchMasterKey(masterKey []byte) (encKey, macKey []byte) { |
| 58 | encKey = hkdfExpand(masterKey, []byte("enc"), 32) |
| 59 | macKey = hkdfExpand(masterKey, []byte("mac"), 32) |
| 60 | return |
| 61 | } |
| 62 | |
| 63 | // HKDFExpandSlice performs HKDF-expand using HMAC-SHA256 to derive a key of the given length. |
| 64 | // Supports multi-block expansion for lengths > 32 bytes. |
| 65 | func HKDFExpandSlice(key, info []byte, length int) []byte { |
| 66 | hashLen := sha256.Size |
| 67 | n := (length + hashLen - 1) / hashLen |
| 68 | out := make([]byte, 0, n*hashLen) |
| 69 | var prev []byte |
| 70 | for i := 1; i <= n; i++ { |
| 71 | h := hmac.New(sha256.New, key) |
| 72 | h.Write(prev) |
| 73 | h.Write(info) |
| 74 | h.Write([]byte{byte(i)}) |
| 75 | prev = h.Sum(nil) |
| 76 | out = append(out, prev...) |
| 77 | } |
| 78 | return out[:length] |
| 79 | } |
| 80 | |
| 81 | // hkdfExpand performs a single-step HKDF-expand using HMAC-SHA256. |
| 82 | func hkdfExpand(key, info []byte, length int) []byte { |
| 83 | return HKDFExpandSlice(key, info, length) |
| 84 | } |
| 85 | |