summaryrefslogtreecommitdiffstats
path: root/internal/cryptoutil/cryptoutil.go
blob: 0429b021812deb8fa76c16828fda616ef42d3b3a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// Package cryptoutil contains friendly wrappers around the algorithms from the
// standard library's crypto package.
package cryptoutil

import (
	"crypto/hkdf"
	"crypto/hmac"
	"crypto/pbkdf2"
	"crypto/rand"
	"crypto/sha512"
	"crypto/subtle"
	"encoding/binary"
	"encoding/hex"
	"errors"
	"io"
)

const (
	defaultIterations = 4718580 // from cmd/finditers

	oneSaltSize = 64
	hashSize    = 64
	certSize    = sha512.Size
)

// SaltSize is the expected salt length for HashIter.
const SaltSize = 2 * oneSaltSize

// 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 = sha512.BlockSize

// An HMAC key can be used to symmetrically sign and verify messages
// using HMAC-SHA512. An HMACKey must be HMACKeyLen bytes.
type HMACKey []byte

// A SignedMessage is a message that has been cryptographically signed
// with HMAC-SHA512.
type SignedMessage []byte

// An EncryptedMessage is encrypted with AES-256-CTR-HMAC-SHA512.
type EncryptedMessage []byte

// HashIter runs PBKDF2-SHA512 for iter iterations. Useful for benchmarking. The
// salt must be SaltSize bytes.
func HashIter(password string, salt []byte, iter int) ([]byte, error) {
	return pbkdf2.Key(sha512.New, password, salt, iter, sha512.Size)
}

func hashWithSalt(password string, salt []byte) (RawKey, []byte, error) {
	hash, err := HashIter(password, salt[:oneSaltSize], defaultIterations)
	if err != nil {
		return RawKey{}, nil, err
	}
	key, err := hkdf.Extract(sha512.New, hash, salt[oneSaltSize:])
	if err != nil {
		return RawKey{}, nil, err
	}
	pwHash, err := hkdf.Expand(sha512.New, key, "pwhash", hashSize)
	if err != nil {
		return RawKey{}, nil, err
	}
	return RawKey{key}, pwHash, nil
}

// Hash hashes a user password using PBKDF2-SHA512.
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 a PasswordHash and returns an
// EncryptionKey derived from the password if successful.
func (h PasswordHash) CheckPassword(password string) (RawKey, error) {
	salt, expectedHash := h[:SaltSize], h[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
}

func (k HMACKey) mac(out, msg []byte, info string) []byte {
	mac := hmac.New(sha512.New, k)
	buf := make([]byte, 0, binary.MaxVarintLen64)
	mac.Write(binary.AppendUvarint(buf, uint64(len(info))))
	io.WriteString(mac, info)
	mac.Write(msg)
	return mac.Sum(out)
}

// Sign generates an HMAC-SHA512 signature and appends it to msg. The info and
// additionalData are also authenticated, but are not included in the returned
// signed message.
func (k HMACKey) Sign(msg []byte, info string) SignedMessage {
	return k.mac(msg, msg, info)
}

// Verify checks whether the given message has a valid signature, and returns
// the raw message if it does. The additionalData must match the data passed
// to Sign.
func (k HMACKey) Verify(msg SignedMessage, info string) ([]byte, bool) {
	if len(msg) < certSize {
		return nil, false
	}
	msg, sig := msg[:len(msg)-certSize], msg[len(msg)-certSize:]
	if !hmac.Equal(sig, k.mac(nil, msg, info)) {
		return nil, false
	}
	return msg, true
}

// EncryptionKey derives an EncryptionKey.
func (k RawKey) EncryptionKey() (EncryptionKey, error) {
	return hkdf.Expand(sha512.New, k.key, "encrypt", sha512.Size)
}