diff options
Diffstat (limited to 'internal/cryptoutil/cryptoutil.go')
| -rw-r--r-- | internal/cryptoutil/cryptoutil.go | 167 |
1 files changed, 167 insertions, 0 deletions
diff --git a/internal/cryptoutil/cryptoutil.go b/internal/cryptoutil/cryptoutil.go new file mode 100644 index 0000000..c5d2a86 --- /dev/null +++ b/internal/cryptoutil/cryptoutil.go @@ -0,0 +1,167 @@ +// Package cryptoutil contains friendly wrappers around the algorithms from the +// standard library's crypto package. +package cryptoutil + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hkdf" + "crypto/hmac" + "crypto/pbkdf2" + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/hex" + "errors" +) + +const ( + defaultIterations = 3749890 // from cmd/finditers + + saltSize = 2 * sha256.Size + hashSize = 32 + certSize = sha256.Size + aesKeySize = 32 + nonceSize = aes.BlockSize +) + +// A PasswordHash must be PasswordHashSize bytes. +const PasswordHashSize = saltSize + hashSize + +// A PasswordHash is derived from the user's password and can be passed to +// CheckPassword to verify if two passwords match. A PasswordHash must be +// PasswordHashSize bytes. +type PasswordHash []byte + +func (h PasswordHash) String() string { return hex.EncodeToString(h) } + +// A RawKey is generated from a password hash and can be used to derive further +// key material. +type RawKey struct { + key []byte +} + +// An HMACKey must be HMACKeySize bytes. +const HMACKeySize = sha256.Size + +// An HMAC key can be used to symmetrically sign and verify messages +// using HMAC-SHA256. An HMACKey must be HMACKeyLen bytes. +type HMACKey []byte + +// A SignedMessage is a message that has been cryptographically signed +// with HMAC-SHA256. +type SignedMessage []byte + +// An EncryptionKey must be EncryptionKeySize bytes. +const EncryptionKeySize = aesKeySize + HMACKeySize + +// An EncryptionKey is used for encrypting and decrypting data. An EncryptionKey +// must be EncryptionKeySize bytes. +type EncryptionKey []byte + +// An EncryptedMessage is encrypted with AES-256-CTR-HMAC-SHA256. +type EncryptedMessage []byte + +// HashIter runs pbkdf2-sha256 for iter iterations. Useful for benchmarking. +func HashIter(password string, salt []byte, iter int) ([]byte, error) { + return pbkdf2.Key(sha256.New, password, salt, iter, sha256.Size) +} + +func hashWithSalt(password string, salt []byte) (RawKey, []byte, error) { + hash, err := HashIter(password, salt[:sha256.Size], defaultIterations) + if err != nil { + return RawKey{}, nil, err + } + key, err := hkdf.Extract(sha256.New, hash, salt[sha256.Size:]) + if err != nil { + return RawKey{}, nil, err + } + pwHash, err := hkdf.Expand(sha256.New, key, "pwhash", hashSize) + if err != nil { + return RawKey{}, nil, err + } + return RawKey{key}, pwHash, nil +} + +// Hash hashes a user password using PBKDF2-SHA256. +func Hash(password string) (PasswordHash, error) { + salt := make([]byte, saltSize, saltSize+hashSize) + rand.Read(salt) + _, pwHash, err := hashWithSalt(password, salt) + if err != nil { + return nil, err + } + return append(salt, pwHash...), nil +} + +// CheckPassword verifies password against expectedHash and returns an +// EncryptionKey derived from the password if successful. +func CheckPassword(password string, expectedHash PasswordHash) (RawKey, error) { + salt, expectedHash := expectedHash[:saltSize], expectedHash[saltSize:] + key, pwHash, err := hashWithSalt(password, salt) + if err != nil { + return RawKey{}, err + } + if subtle.ConstantTimeCompare(pwHash, expectedHash) == 0 { + return RawKey{}, errors.New("incorrect password") + } + return key, nil +} + +// Sign generates an HMAC-SHA256 signature and appends it to msg. +func (k HMACKey) Sign(msg []byte) SignedMessage { + mac := hmac.New(sha256.New, k) + mac.Write(msg) + return mac.Sum(msg) +} + +// Verify checks whether the given message has a valid signature, and returns +// the raw message if it does. +func (k HMACKey) Verify(msg SignedMessage) ([]byte, bool) { + if len(msg) < certSize { + return nil, false + } + msg, sig := msg[:len(msg)-certSize], msg[len(msg)-certSize:] + mac := hmac.New(sha256.New, k) + mac.Write(msg) + if !hmac.Equal(sig, mac.Sum(nil)) { + return nil, false + } + return msg, true +} + +// EncryptionKey derives an EncryptionKey. +func (k RawKey) EncryptionKey() (EncryptionKey, error) { + return hkdf.Expand(sha256.New, k.key, "encrypt", EncryptionKeySize) +} + +// Encrypt encrypts a message with AES-256-CTR-HMAC-SHA256. +func (k EncryptionKey) Encrypt(msg []byte) (EncryptedMessage, error) { + aesKey, hmacKey := k[:aesKeySize], HMACKey(k[aesKeySize:]) + block, err := aes.NewCipher(aesKey) + if err != nil { + return nil, err + } + cipherText := make([]byte, len(msg)+nonceSize, len(msg)+nonceSize+certSize) + nonce := cipherText[len(msg) : len(msg)+nonceSize] + rand.Read(nonce) + cipher.NewCTR(block, nonce).XORKeyStream(cipherText, msg) + return EncryptedMessage(hmacKey.Sign(cipherText)), nil +} + +// Decrypt verifies and decrypts an encrypted message. It overwrites msg with +// the resulting plain text. +func (k EncryptionKey) Decrypt(msg EncryptedMessage) ([]byte, error) { + aesKey, hmacKey := k[:aesKeySize], HMACKey(k[aesKeySize:]) + msg, ok := hmacKey.Verify(SignedMessage(msg)) + if !ok { + return nil, errors.New("bad signature") + } + block, err := aes.NewCipher(aesKey) + if err != nil { + return nil, err + } + msg, nonce := msg[:len(msg)-nonceSize], msg[len(msg)-nonceSize:] + cipher.NewCTR(block, nonce).XORKeyStream(msg, msg) + return msg, nil +} |
