golib/crypto/passphrase/passphrase.go

80 lines
2.0 KiB
Go

package passphrase
import (
"crypto/pbkdf2"
"crypto/rand"
"crypto/sha512"
"errors"
"strings"
"github.com/therootcompany/golib/encoding/base2048"
)
var (
ErrInvalidWordCount = errors.New("passphrase must contain 12, 15, 18, 21 or 24 words")
ErrInvalidEntropyBits = errors.New("entropy must be 128, 160, 192, 224 or 256 bits (16-32 bytes)")
)
const (
MagicString = "mnemonic"
)
// Generate creates a new passphrase with the specified entropy bits (128, 160, 192, 224, or 256)
// If the bit size doesn't match a known size it returns the phrase and ErrInvalidEntropyBits.
func Generate(bits int) (string, error) {
var err error
var byteLen int
switch bits {
case 128:
byteLen = 16
case 160:
byteLen = 20
case 192:
byteLen = 24
case 224:
byteLen = 28
case 256:
byteLen = 32
default:
byteLen = bits / 8
if bits%8 != 0 {
byteLen += 1
}
err = ErrInvalidEntropyBits
}
entropy := make([]byte, byteLen)
if _, err := rand.Read(entropy); err != nil {
return "", err
}
return base2048.EncodeToString(entropy), err
}
// SeedFrom converts a base2048 passphrase into a 512-bit seed (as per BIP-39)
// using PBKDF2-HMAC-SHA512 with 2048 iterations with an optional passphrase for salt.
// If the input doesn't pass verification, it returns the seed bytes and base2048.ErrChecksumMismatch.
// If the number of words isn't 12, 15, 18, 21 or 24, it returns the seed bytes and ErrInvalidWordCount.
// strings.Fields() is used to split on runs of whitespace.
// Non-ASCII text SHOULD first be normalized with norm.NFKD.String(s)
func SeedFrom(recoveryPhrase, saltWord string) ([]byte, error) {
salt := []byte(MagicString + saltWord)
recoveryWords := strings.Fields(recoveryPhrase)
iterations := 2048
keySize := 64
seed, err := pbkdf2.Key(sha512.New, recoveryPhrase, salt, iterations, keySize)
if err == nil {
_, err = base2048.DecodeWords(recoveryWords)
if err == nil {
switch len(recoveryWords) {
case 12, 15, 18, 21, 24:
// great
default:
err = ErrInvalidWordCount
}
}
}
return seed, err
}