mirror of
https://github.com/cloudflare/redoctober.git
synced 2026-09-26 00:54:14 +00:00
608 lines
14 KiB
Go
608 lines
14 KiB
Go
// Package passvault manages the vault containing user records on
|
|
// disk. It contains usernames and associated passwords which are
|
|
// stored hashed (with salt) using scrypt.
|
|
//
|
|
// Copyright (c) 2013 CloudFlare, Inc.
|
|
|
|
package passvault
|
|
|
|
import (
|
|
"bytes"
|
|
"code.google.com/p/go.crypto/scrypt"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha1"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"github.com/cloudflare/redoctober/padding"
|
|
"io/ioutil"
|
|
"math/big"
|
|
mrand "math/rand"
|
|
"os"
|
|
)
|
|
|
|
// Constants for record type
|
|
const (
|
|
AESRecord = "AES"
|
|
RSARecord = "RSA"
|
|
ECCRecord = "ECC"
|
|
)
|
|
|
|
// Constants for scrypt
|
|
const (
|
|
KEYLENGTH = 16 // 16-byte output from scrypt
|
|
N = 16384 // Cost parameter
|
|
R = 8 // Block size
|
|
P = 1 // Parallelization factor
|
|
|
|
DEFAULT_VERSION = 1
|
|
)
|
|
|
|
// Path of current vault
|
|
var localPath string
|
|
|
|
// PasswordRecord is the structure used to store password and key
|
|
// material for a single user name. It is written and read from
|
|
// storage in JSON format.
|
|
type PasswordRecord struct {
|
|
Type string
|
|
PasswordSalt []byte
|
|
HashedPassword []byte
|
|
KeySalt []byte
|
|
AESKey []byte
|
|
RSAKey struct {
|
|
RSAExp []byte
|
|
RSAExpIV []byte
|
|
RSAPrimeP []byte
|
|
RSAPrimePIV []byte
|
|
RSAPrimeQ []byte
|
|
RSAPrimeQIV []byte
|
|
RSAPublic rsa.PublicKey
|
|
}
|
|
Admin bool
|
|
}
|
|
|
|
// diskRecords is the structure used to read and write a JSON file
|
|
// containing the contents of a password vault
|
|
type diskRecords struct {
|
|
Version int
|
|
VaultId int
|
|
HmacKey []byte
|
|
Passwords map[string]PasswordRecord
|
|
}
|
|
|
|
// records is the set of encrypted records read from disk and
|
|
// unmarshalled
|
|
var records diskRecords
|
|
|
|
// Summary is a minmial account summary.
|
|
type Summary struct {
|
|
Admin bool
|
|
Type string
|
|
}
|
|
|
|
func init() {
|
|
// seed math.random from crypto.random
|
|
seedBytes, _ := makeRandom(8)
|
|
seedBuf := bytes.NewBuffer(seedBytes)
|
|
n64, _ := binary.ReadVarint(seedBuf)
|
|
mrand.Seed(n64)
|
|
}
|
|
|
|
// hashPassword takes a password and derives a scrypt salted and hashed
|
|
// version
|
|
func hashPassword(password string, salt []byte) ([]byte, error) {
|
|
return scrypt.Key([]byte(password), salt, N, R, P, KEYLENGTH)
|
|
}
|
|
|
|
// makeRandom is a helper that makes a new buffer full of random data
|
|
func makeRandom(length int) ([]byte, error) {
|
|
bytes := make([]byte, length)
|
|
_, err := rand.Read(bytes)
|
|
return bytes, err
|
|
}
|
|
|
|
// encryptRSARecord takes an RSA private key and encrypts it with
|
|
// a password key
|
|
func encryptRSARecord(newRec *PasswordRecord, rsaPriv *rsa.PrivateKey, passKey []byte) (err error) {
|
|
if newRec.RSAKey.RSAExpIV, err = makeRandom(16); err != nil {
|
|
return
|
|
}
|
|
|
|
paddedExponent := padding.AddPadding(rsaPriv.D.Bytes())
|
|
if newRec.RSAKey.RSAExp, err = encryptCBC(paddedExponent, newRec.RSAKey.RSAExpIV, passKey); err != nil {
|
|
return
|
|
}
|
|
|
|
if newRec.RSAKey.RSAPrimePIV, err = makeRandom(16); err != nil {
|
|
return
|
|
}
|
|
|
|
paddedPrimeP := padding.AddPadding(rsaPriv.Primes[0].Bytes())
|
|
if newRec.RSAKey.RSAPrimeP, err = encryptCBC(paddedPrimeP, newRec.RSAKey.RSAPrimePIV, passKey); err != nil {
|
|
return
|
|
}
|
|
|
|
if newRec.RSAKey.RSAPrimeQIV, err = makeRandom(16); err != nil {
|
|
return
|
|
}
|
|
|
|
paddedPrimeQ := padding.AddPadding(rsaPriv.Primes[1].Bytes())
|
|
newRec.RSAKey.RSAPrimeQ, err = encryptCBC(paddedPrimeQ, newRec.RSAKey.RSAPrimeQIV, passKey)
|
|
return
|
|
}
|
|
|
|
// createPasswordRec creates a new record from a username and password
|
|
func createPasswordRec(password string, admin bool) (newRec PasswordRecord, err error) {
|
|
newRec.Type = RSARecord
|
|
|
|
if newRec.PasswordSalt, err = makeRandom(16); err != nil {
|
|
return
|
|
}
|
|
|
|
if newRec.HashedPassword, err = hashPassword(password, newRec.PasswordSalt); err != nil {
|
|
return
|
|
}
|
|
|
|
if newRec.KeySalt, err = makeRandom(16); err != nil {
|
|
return
|
|
}
|
|
|
|
passKey, err := derivePasswordKey(password, newRec.KeySalt)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// generate a key pair
|
|
rsaPriv, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// encrypt RSA key with password key
|
|
if err = encryptRSARecord(&newRec, rsaPriv, passKey); err != nil {
|
|
return
|
|
}
|
|
|
|
newRec.RSAKey.RSAPublic = rsaPriv.PublicKey
|
|
|
|
// encrypt AES key with password key
|
|
aesKey, err := makeRandom(16)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if newRec.AESKey, err = encryptECB(aesKey, passKey); err != nil {
|
|
return
|
|
}
|
|
|
|
newRec.Admin = admin
|
|
|
|
return
|
|
}
|
|
|
|
// derivePasswordKey generates a key from a password (and salt) using
|
|
// scrypt
|
|
func derivePasswordKey(password string, keySalt []byte) ([]byte, error) {
|
|
return scrypt.Key([]byte(password), keySalt, N, R, P, KEYLENGTH)
|
|
}
|
|
|
|
// decryptECB decrypts bytes using a key in AES ECB mode.
|
|
func decryptECB(data, key []byte) (decryptedData []byte, err error) {
|
|
aesCrypt, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
decryptedData = make([]byte, len(data))
|
|
aesCrypt.Decrypt(decryptedData, data)
|
|
|
|
return
|
|
}
|
|
|
|
// encryptECB encrypts bytes using a key in AES ECB mode.
|
|
func encryptECB(data, key []byte) (encryptedData []byte, err error) {
|
|
aesCrypt, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
encryptedData = make([]byte, len(data))
|
|
aesCrypt.Encrypt(encryptedData, data)
|
|
|
|
return
|
|
}
|
|
|
|
// decryptCBC decrypt bytes using a key and IV with AES in CBC mode.
|
|
func decryptCBC(data, iv, key []byte) (decryptedData []byte, err error) {
|
|
aesCrypt, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return
|
|
}
|
|
ivBytes := append([]byte{}, iv...)
|
|
|
|
decryptedData = make([]byte, len(data))
|
|
aesCBC := cipher.NewCBCDecrypter(aesCrypt, ivBytes)
|
|
aesCBC.CryptBlocks(decryptedData, data)
|
|
|
|
return
|
|
}
|
|
|
|
// encryptCBC encrypt data using a key and IV with AES in CBC mode.
|
|
func encryptCBC(data, iv, key []byte) (encryptedData []byte, err error) {
|
|
aesCrypt, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return
|
|
}
|
|
ivBytes := append([]byte{}, iv...)
|
|
|
|
encryptedData = make([]byte, len(data))
|
|
aesCBC := cipher.NewCBCEncrypter(aesCrypt, ivBytes)
|
|
aesCBC.CryptBlocks(encryptedData, data)
|
|
|
|
return
|
|
}
|
|
|
|
// InitFromDisk reads the record from disk and initialize global context.
|
|
func InitFromDisk(path string) error {
|
|
jsonDiskRecord, err := ioutil.ReadFile(path)
|
|
|
|
// It's OK for the file to be missing, we'll create it later if
|
|
// anything is added.
|
|
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
|
|
// Initialized so that we can determine later if anything was read
|
|
// from the file.
|
|
|
|
records.Version = 0
|
|
|
|
if len(jsonDiskRecord) != 0 {
|
|
if err = json.Unmarshal(jsonDiskRecord, &records); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
formatErr := errors.New("Format error")
|
|
for _, rec := range records.Passwords {
|
|
if len(rec.PasswordSalt) != 16 {
|
|
return formatErr
|
|
}
|
|
if len(rec.HashedPassword) != 16 {
|
|
return formatErr
|
|
}
|
|
if len(rec.KeySalt) != 16 {
|
|
return formatErr
|
|
}
|
|
if rec.Type == AESRecord {
|
|
if len(rec.AESKey) != 16 {
|
|
return formatErr
|
|
}
|
|
}
|
|
if rec.Type == RSARecord {
|
|
if len(rec.RSAKey.RSAExp) == 0 || len(rec.RSAKey.RSAExp)%16 != 0 {
|
|
return formatErr
|
|
}
|
|
if len(rec.RSAKey.RSAPrimeP) == 0 || len(rec.RSAKey.RSAPrimeP)%16 != 0 {
|
|
return formatErr
|
|
}
|
|
if len(rec.RSAKey.RSAPrimeQ) == 0 || len(rec.RSAKey.RSAPrimeQ)%16 != 0 {
|
|
return formatErr
|
|
}
|
|
if len(rec.RSAKey.RSAExpIV) != 16 {
|
|
return formatErr
|
|
}
|
|
if len(rec.RSAKey.RSAPrimePIV) != 16 {
|
|
return formatErr
|
|
}
|
|
if len(rec.RSAKey.RSAPrimeQIV) != 16 {
|
|
return formatErr
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the Version field is 0 then it indicates that nothing was
|
|
// read from the file and so it needs to be initialized.
|
|
|
|
if records.Version == 0 {
|
|
records.Version = DEFAULT_VERSION
|
|
records.VaultId = mrand.Int()
|
|
records.HmacKey, err = makeRandom(16)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
records.Passwords = make(map[string]PasswordRecord)
|
|
}
|
|
|
|
localPath = path
|
|
|
|
return nil
|
|
}
|
|
|
|
// WriteRecordsToDisk saves the current state of the records to disk.
|
|
func WriteRecordsToDisk() error {
|
|
if !IsInitialized() {
|
|
return errors.New("Path not initialized")
|
|
}
|
|
|
|
if jsonDiskRecord, err := json.Marshal(records); err == nil {
|
|
return ioutil.WriteFile(localPath, jsonDiskRecord, 0644)
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// AddNewRecord adds a new record for a given username and password.
|
|
func AddNewRecord(name, password string, admin bool) (PasswordRecord, error) {
|
|
if pr, err := createPasswordRec(password, admin); err == nil {
|
|
SetRecord(pr, name)
|
|
return pr, WriteRecordsToDisk()
|
|
} else {
|
|
return pr, err
|
|
}
|
|
}
|
|
|
|
// ChangePassword changes the password for a given user.
|
|
func ChangePassword(name, password, newPassword string) (err error) {
|
|
pr, ok := GetRecord(name)
|
|
if !ok {
|
|
err = errors.New("Record not present")
|
|
return
|
|
}
|
|
if err = pr.ValidatePassword(password); err != nil {
|
|
return
|
|
}
|
|
|
|
// decrypt key
|
|
var key []byte
|
|
var rsaKey rsa.PrivateKey
|
|
if pr.Type == AESRecord {
|
|
key, err = pr.GetKeyAES(password)
|
|
if err != nil {
|
|
return
|
|
}
|
|
} else if pr.Type == RSARecord {
|
|
rsaKey, err = pr.GetKeyRSA(password)
|
|
if err != nil {
|
|
return
|
|
}
|
|
} else {
|
|
err = errors.New("Unkown record type")
|
|
return
|
|
}
|
|
|
|
// add the password salt and hash
|
|
if pr.PasswordSalt, err = makeRandom(16); err != nil {
|
|
return
|
|
}
|
|
if pr.HashedPassword, err = hashPassword(newPassword, pr.PasswordSalt); err != nil {
|
|
return
|
|
}
|
|
|
|
if pr.KeySalt, err = makeRandom(16); err != nil {
|
|
return
|
|
}
|
|
newPassKey, err := derivePasswordKey(newPassword, pr.KeySalt)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// encrypt original key with new password
|
|
if pr.Type == AESRecord {
|
|
pr.AESKey, err = encryptECB(key, newPassKey)
|
|
if err != nil {
|
|
return
|
|
}
|
|
} else if pr.Type == RSARecord {
|
|
// encrypt RSA key with password key
|
|
err = encryptRSARecord(&pr, &rsaKey, newPassKey)
|
|
if err != nil {
|
|
return
|
|
}
|
|
} else {
|
|
err = errors.New("Unkown record type")
|
|
return
|
|
}
|
|
|
|
SetRecord(pr, name)
|
|
|
|
return WriteRecordsToDisk()
|
|
}
|
|
|
|
// DeleteRecord deletes a given record.
|
|
func DeleteRecord(name string) error {
|
|
if _, ok := GetRecord(name); ok {
|
|
delete(records.Passwords, name)
|
|
return nil
|
|
}
|
|
|
|
return errors.New("Record missing")
|
|
}
|
|
|
|
// RevokeRecord removes admin status from a record.
|
|
func RevokeRecord(name string) error {
|
|
if rec, ok := GetRecord(name); ok {
|
|
rec.Admin = false
|
|
SetRecord(rec, name)
|
|
return nil
|
|
}
|
|
|
|
return errors.New("Record missing")
|
|
}
|
|
|
|
// MakeAdmin adds admin status to a given record.
|
|
func MakeAdmin(name string) error {
|
|
if rec, ok := GetRecord(name); ok {
|
|
rec.Admin = true
|
|
SetRecord(rec, name)
|
|
return nil
|
|
}
|
|
|
|
return errors.New("Record missing")
|
|
}
|
|
|
|
// SetRecord puts a record into the global status.
|
|
func SetRecord(pr PasswordRecord, name string) {
|
|
records.Passwords[name] = pr
|
|
}
|
|
|
|
// GetRecord returns a record given a name.
|
|
func GetRecord(name string) (PasswordRecord, bool) {
|
|
dpr, found := records.Passwords[name]
|
|
return dpr, found
|
|
}
|
|
|
|
// GetVaultId returns the id of the current vault.
|
|
func GetVaultId() (id int, err error) {
|
|
if !IsInitialized() {
|
|
return 0, errors.New("Path not initialized")
|
|
}
|
|
|
|
return records.VaultId, nil
|
|
}
|
|
|
|
// GetHmacKey returns the hmac key of the current vault.
|
|
func GetHmacKey() (key []byte, err error) {
|
|
if !IsInitialized() {
|
|
return nil, errors.New("Path not initialized")
|
|
}
|
|
|
|
return records.HmacKey, nil
|
|
}
|
|
|
|
// IsInitialized returns true if the disk vault has been loaded.
|
|
func IsInitialized() bool {
|
|
return localPath != ""
|
|
}
|
|
|
|
// NumRecords returns the number of records in the vault.
|
|
func NumRecords() int {
|
|
return len(records.Passwords)
|
|
}
|
|
|
|
// GetSummary returns a summary of the records on disk.
|
|
func GetSummary() (summary map[string]Summary) {
|
|
summary = make(map[string]Summary)
|
|
for name, pass := range records.Passwords {
|
|
summary[name] = Summary{pass.Admin, pass.Type}
|
|
}
|
|
return
|
|
}
|
|
|
|
// IsAdmin returns the admin status of the PasswordRecord.
|
|
func (pr PasswordRecord) IsAdmin() bool {
|
|
return pr.Admin
|
|
}
|
|
|
|
// GetType returns the type status of the PasswordRecord.
|
|
func (pr PasswordRecord) GetType() string {
|
|
return pr.Type
|
|
}
|
|
|
|
// EncryptKey encrypts a 16-byte key with the RSA key of the record.
|
|
func (pr PasswordRecord) EncryptKey(in []byte) (out []byte, err error) {
|
|
return rsa.EncryptOAEP(sha1.New(), rand.Reader, &pr.RSAKey.RSAPublic, in, nil)
|
|
}
|
|
|
|
// GetKeyAES returns the 16-byte key of the record.
|
|
func (pr PasswordRecord) GetKeyAES(password string) (key []byte, err error) {
|
|
if pr.Type != AESRecord {
|
|
return nil, errors.New("Invalid function for record type")
|
|
}
|
|
|
|
err = pr.ValidatePassword(password)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
passKey, err := derivePasswordKey(password, pr.KeySalt)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return decryptECB(pr.AESKey, passKey)
|
|
}
|
|
|
|
// GetKeyAES returns the RSA public key of the record.
|
|
func (pr PasswordRecord) GetKeyRSAPub() (out *rsa.PublicKey, err error) {
|
|
if pr.Type != RSARecord {
|
|
return out, errors.New("Invalid function for record type")
|
|
}
|
|
return &pr.RSAKey.RSAPublic, err
|
|
}
|
|
|
|
// GetKeyAES returns the RSA private key of the record given the correct password.
|
|
func (pr PasswordRecord) GetKeyRSA(password string) (key rsa.PrivateKey, err error) {
|
|
if pr.Type != RSARecord {
|
|
return key, errors.New("Invalid function for record type")
|
|
}
|
|
|
|
err = pr.ValidatePassword(password)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
passKey, err := derivePasswordKey(password, pr.KeySalt)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
rsaExponentPadded, err := decryptCBC(pr.RSAKey.RSAExp, pr.RSAKey.RSAExpIV, passKey)
|
|
if err != nil {
|
|
return
|
|
}
|
|
rsaExponent, err := padding.RemovePadding(rsaExponentPadded)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
rsaPrimePPadded, err := decryptCBC(pr.RSAKey.RSAPrimeP, pr.RSAKey.RSAPrimePIV, passKey)
|
|
if err != nil {
|
|
return
|
|
}
|
|
rsaPrimeP, err := padding.RemovePadding(rsaPrimePPadded)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
rsaPrimeQPadded, err := decryptCBC(pr.RSAKey.RSAPrimeQ, pr.RSAKey.RSAPrimeQIV, passKey)
|
|
if err != nil {
|
|
return
|
|
}
|
|
rsaPrimeQ, err := padding.RemovePadding(rsaPrimeQPadded)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
key.PublicKey = pr.RSAKey.RSAPublic
|
|
key.D = big.NewInt(0).SetBytes(rsaExponent)
|
|
key.Primes = []*big.Int{big.NewInt(0), big.NewInt(0)}
|
|
key.Primes[0].SetBytes(rsaPrimeP)
|
|
key.Primes[1].SetBytes(rsaPrimeQ)
|
|
|
|
err = key.Validate()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// ValidatePassword returns an error if the password is incorrect.
|
|
func (pr PasswordRecord) ValidatePassword(password string) error {
|
|
if h, err := hashPassword(password, pr.PasswordSalt); err != nil {
|
|
return err
|
|
} else {
|
|
if bytes.Compare(h, pr.HashedPassword) != 0 {
|
|
return errors.New("Wrong Password")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|