mirror of
https://github.com/1f349/twofactor.git
synced 2024-12-22 15:34:09 +00:00
72a472700b
The counter needs to be represented in bigendian format. Unfortunately with commit #00045cb I made the unfortunate choice to swap the endiannes from big-endian to little-endian. This broke the functionality for certain counters. - [x] Added Go vendoring - [x] Bumped version of golang in travis yml file - [x] Removed conversion files and instead used import of convert sec51 external library
512 lines
16 KiB
Go
512 lines
16 KiB
Go
package cryptoengine
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"golang.org/x/crypto/nacl/box"
|
|
"golang.org/x/crypto/nacl/secretbox"
|
|
"log"
|
|
"math"
|
|
"net/url"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
const (
|
|
nonceSize = 24 // this is the nonce size, required by NaCl
|
|
keySize = 32 // this is the nonce size, required by NaCl
|
|
rotateSaltAfterDays = 7 // this is the amount of days the salt is valid - if it crosses this amount a new salt is generated
|
|
tcpVersion = 0 // this is the current TCP version
|
|
)
|
|
|
|
var (
|
|
KeySizeError = errors.New(fmt.Sprintf("The provisioned key size is less than: %d\n", keySize))
|
|
KeyNotValidError = errors.New("The provisioned public key is not valid")
|
|
SaltGenerationError = errors.New("Could not generate random salt")
|
|
KeyGenerationError = errors.New("Could not generate random key")
|
|
MessageDecryptionError = errors.New("Could not verify the message. Message has been tempered with!")
|
|
MessageParsingError = errors.New("Could not parse the Message from bytes")
|
|
messageEmpty = errors.New("Can not encrypt an empty message")
|
|
whiteSpaceRegEx = regexp.MustCompile("\\s")
|
|
emptyKey = make([]byte, keySize)
|
|
|
|
// salt for derivating keys
|
|
saltSuffixFormat = "%s_salt.key" // this is the salt file,for instance: sec51_salt.key
|
|
|
|
// secret key for symmetric encryption
|
|
secretSuffixFormat = "%s_secret.key" // this is the secret key crypto file, for instance: sec51_secret.key
|
|
|
|
// asymmetric keys
|
|
publicKeySuffixFormat = "%s_public.key" // this is the public key crypto file,for instance: sec51_public.key
|
|
privateSuffixFormat = "%s_private.key" // this is the private key crypto file,for instance: sec51_priovate.key
|
|
|
|
// nonce secret key
|
|
nonceSuffixFormat = "%s_nonce.key" // this is the secret key crypto file used for generating nonces,for instance: sec51_nonce.key
|
|
)
|
|
|
|
// This is the basic object which needs to be instanciated for encrypting messages
|
|
// either via public key cryptography or private key cryptography
|
|
// The object has the methods necessary to execute all the needed functions to encrypt and decrypt a message, both with symmetric and asymmetric
|
|
// crypto
|
|
type CryptoEngine struct {
|
|
context string // this is the context used for the key derivation function and for namespacing the key files
|
|
publicKey [keySize]byte // cached asymmetric public key
|
|
privateKey [keySize]byte // cached asymmetric private key
|
|
secretKey [keySize]byte // secret key used for symmetric encryption
|
|
salt [keySize]byte // salt for deriving the random nonces
|
|
nonceKey [keySize]byte // this key is used for deriving the random nonces. It's different from the privateKey
|
|
mutex sync.Mutex // this mutex is used ti make sure that in case the engine is used by multiple thread the pre-shared key is correctly generated
|
|
preSharedKeysMap map[string][keySize]byte // this map holds the combination hash of peer public key as the map key and the preshared key as value used to encrypt
|
|
counter uint64 // this is the counter which is appended to the HKDF at each call
|
|
counterMutex sync.Mutex // this is the counter mutex for a safe incrementation (TODO: look into atomic)
|
|
}
|
|
|
|
// This function initialize all the necessary information to carry out a secure communication
|
|
// either via public key cryptography or secret key cryptography.
|
|
// The peculiarity is that the user of this package needs to take care of only one parameter, the communicationIdentifier.
|
|
// It defines a unique set of keys between the application and the communicationIdentifier unique end point.
|
|
// IMPORTANT: The parameter communicationIdentifier defines several assumptions the code use:
|
|
// - it names the secret key files with the comuncationIdentifier prefix. This means that if you want to have different secret keys
|
|
// with different end points, you can differrentiate the key by having different unique communicationIdentifier.
|
|
// It, also, loads the already created keys back in memory based on the communicationIdentifier
|
|
// - it does the same with the asymmetric keys
|
|
// The communicationIdentifier parameter is URL unescape, trimmed, set to lower case and all the white spaces are replaced with an underscore.
|
|
// The publicKey parameter can be nil. In that case the CryptoEngine assumes it has been instanciated for symmetric crypto usage.
|
|
func InitCryptoEngine(communicationIdentifier string) (*CryptoEngine, error) {
|
|
// define an error object
|
|
var err error
|
|
// create a new crypto engine object
|
|
ce := new(CryptoEngine)
|
|
|
|
// sanitize the communicationIdentifier
|
|
ce.context = sanitizeIdentifier(communicationIdentifier)
|
|
|
|
// load or generate the salt
|
|
salt, err := loadSalt(ce.context)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ce.salt = salt
|
|
|
|
// load or generate the corresponding public/private key pair
|
|
ce.publicKey, ce.privateKey, err = loadKeyPairs(ce.context)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// load or generate the secret key
|
|
secretKey, err := loadSecretKey(ce.context)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ce.secretKey = secretKey
|
|
|
|
// load the nonce key
|
|
nonceKey, err := loadNonceKey(ce.context)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ce.nonceKey = nonceKey
|
|
|
|
// init the map
|
|
ce.preSharedKeysMap = make(map[string][keySize]byte)
|
|
|
|
// finally return the CryptoEngine instance
|
|
return ce, nil
|
|
|
|
}
|
|
|
|
// this function reads nonceSize random data
|
|
func generateSalt() ([keySize]byte, error) {
|
|
var data32 [keySize]byte
|
|
data := make([]byte, keySize)
|
|
_, err := rand.Read(data)
|
|
if err != nil {
|
|
return data32, err
|
|
}
|
|
total := copy(data32[:], data)
|
|
if total != keySize {
|
|
return data32, SaltGenerationError
|
|
}
|
|
return data32, nil
|
|
}
|
|
|
|
// this function reads keySize random data
|
|
func generateSecretKey() ([keySize]byte, error) {
|
|
var data32 [keySize]byte
|
|
data := make([]byte, keySize)
|
|
_, err := rand.Read(data)
|
|
if err != nil {
|
|
return data32, err
|
|
}
|
|
total := copy(data32[:], data[:keySize])
|
|
if total != keySize {
|
|
return data32, KeyGenerationError
|
|
}
|
|
return data32, nil
|
|
}
|
|
|
|
// load the salt random bytes from the id_salt.key
|
|
// if the file does not exist, create a new one
|
|
// if the file is older than N days (default 2) generate a new one and overwrite the old
|
|
// TODO: rotate the salt file
|
|
func loadSalt(id string) ([keySize]byte, error) {
|
|
|
|
var salt [keySize]byte
|
|
|
|
saltFile := fmt.Sprintf(saltSuffixFormat, id)
|
|
if keyFileExists(saltFile) {
|
|
return readKey(saltFile, keysFolderPrefixFormat)
|
|
}
|
|
|
|
// generate the random salt
|
|
salt, err := generateSalt()
|
|
if err != nil {
|
|
return salt, err
|
|
}
|
|
|
|
// write the salt to the file with its prefix
|
|
if err := writeKey(saltFile, keysFolderPrefixFormat, salt[:]); err != nil {
|
|
return salt, err
|
|
}
|
|
|
|
// return the salt and no error
|
|
return salt, nil
|
|
}
|
|
|
|
// load the key random bytes from the id_secret.key
|
|
// if the file does not exist, create a new one
|
|
func loadSecretKey(id string) ([keySize]byte, error) {
|
|
|
|
var key [keySize]byte
|
|
|
|
keyFile := fmt.Sprintf(secretSuffixFormat, id)
|
|
if keyFileExists(keyFile) {
|
|
return readKey(keyFile, keysFolderPrefixFormat)
|
|
}
|
|
|
|
// generate the random salt
|
|
key, err := generateSecretKey()
|
|
if err != nil {
|
|
return key, err
|
|
}
|
|
|
|
// write the salt to the file with its prefix
|
|
if err := writeKey(keyFile, keysFolderPrefixFormat, key[:]); err != nil {
|
|
return key, err
|
|
}
|
|
|
|
// return the salt and no error
|
|
return key, nil
|
|
}
|
|
|
|
// load the nonce key random bytes from the id_nonce.key
|
|
// if the file does not exist, create a new one
|
|
func loadNonceKey(id string) ([keySize]byte, error) {
|
|
|
|
var nonceKey [keySize]byte
|
|
|
|
nonceFile := fmt.Sprintf(nonceSuffixFormat, id)
|
|
if keyFileExists(nonceFile) {
|
|
return readKey(nonceFile, keysFolderPrefixFormat)
|
|
}
|
|
|
|
// generate the random salt
|
|
nonceKey, err := generateSecretKey()
|
|
if err != nil {
|
|
return nonceKey, err
|
|
}
|
|
|
|
// write the salt to the file with its prefix
|
|
if err := writeKey(nonceFile, keysFolderPrefixFormat, nonceKey[:]); err != nil {
|
|
return nonceKey, err
|
|
}
|
|
|
|
// return the salt and no error
|
|
return nonceKey, nil
|
|
}
|
|
|
|
// load the key pair, public and private keys, the id_public.key, id_private.key
|
|
// if the files do not exist, create them
|
|
// Returns the publicKey, privateKey, error
|
|
func loadKeyPairs(id string) ([keySize]byte, [keySize]byte, error) {
|
|
|
|
var private [keySize]byte
|
|
var public [keySize]byte
|
|
var err error
|
|
|
|
// try to load the private key
|
|
privateFile := fmt.Sprintf(privateSuffixFormat, id)
|
|
if keyFileExists(privateFile) {
|
|
if private, err = readKey(privateFile, keysFolderPrefixFormat); err != nil {
|
|
return public, private, err
|
|
}
|
|
}
|
|
// try to load the public key and if it succeed, then return both the keys
|
|
publicFile := fmt.Sprintf(publicKeySuffixFormat, id)
|
|
if keyFileExists(publicFile) {
|
|
if public, err = readKey(publicFile, keysFolderPrefixFormat); err != nil {
|
|
return public, private, err
|
|
}
|
|
|
|
// if we reached here, it means that both the private and the public key
|
|
// existed and loaded successfully
|
|
return public, private, err
|
|
}
|
|
|
|
// if we reached here then, we need to cerate the key pair
|
|
tempPublic, tempPrivate, err := box.GenerateKey(rand.Reader)
|
|
|
|
// check for errors first, otherwise continue and store the keys to files
|
|
if err != nil {
|
|
return public, private, err
|
|
}
|
|
// dereference the pointers
|
|
public = *tempPublic
|
|
private = *tempPrivate
|
|
|
|
// write the public key first
|
|
if err := writeKey(publicFile, keysFolderPrefixFormat, public[:]); err != nil {
|
|
return public, private, err
|
|
}
|
|
|
|
// write the private
|
|
if err := writeKey(privateFile, keysFolderPrefixFormat, private[:]); err != nil {
|
|
// delete the public key, otherwise we remain in an unwanted state
|
|
// the delete can fail as well, therefore we print an error
|
|
if err := deleteFile(publicFile); err != nil {
|
|
log.Printf("[SEVERE] - The private key for asymmetric encryption, %s, failed to be persisted. \nWhile trying to cleanup also the public key previosuly stored, %s, the operation failed as well.\nWe are now in an unrecoverable state.Please delete both files manually: %s - %s", privateFile, publicFile, privateFile, publicFile)
|
|
return public, private, err
|
|
}
|
|
return public, private, err
|
|
}
|
|
|
|
// return the data
|
|
return public, private, err
|
|
|
|
}
|
|
|
|
// Sanitizes the input of the communicationIdentifier
|
|
// The input is URL unescape, trimmed, set to lower case and all the white spaces are replaced with an underscore.
|
|
// TODO: evaluate the QueryUnescape error
|
|
func sanitizeIdentifier(id string) string {
|
|
// unescape in case it;s URL encoded
|
|
unescaped, _ := url.QueryUnescape(id)
|
|
// trim white spaces
|
|
trimmed := strings.TrimSpace(unescaped)
|
|
// make lower case
|
|
lowered := strings.ToLower(trimmed)
|
|
// replace the white spaces with _
|
|
cleaned := whiteSpaceRegEx.ReplaceAllLiteralString(lowered, "_")
|
|
return cleaned
|
|
}
|
|
|
|
func (engine *CryptoEngine) fetchAndIncrement() string {
|
|
engine.counterMutex.Lock()
|
|
defer engine.counterMutex.Unlock()
|
|
|
|
// first read the current value
|
|
// reset the counter
|
|
if engine.counter == math.MaxUint64 {
|
|
engine.counter = 0
|
|
}
|
|
|
|
// convert the counter to string
|
|
counterString := strconv.FormatUint(engine.counter, 10)
|
|
|
|
// increment the counter
|
|
engine.counter += 1
|
|
|
|
return counterString
|
|
}
|
|
|
|
// Gives access to the public key
|
|
func (engine *CryptoEngine) PublicKey() []byte {
|
|
return engine.publicKey[:]
|
|
}
|
|
|
|
// This method accepts a message , then encrypts its Version+Type+Text using a symmetric key
|
|
func (engine *CryptoEngine) NewEncryptedMessage(msg message) (EncryptedMessage, error) {
|
|
|
|
m := EncryptedMessage{}
|
|
|
|
// derive nonce
|
|
nonce, err := deriveNonce(engine.nonceKey, engine.salt, engine.context, engine.fetchAndIncrement())
|
|
if err != nil {
|
|
return m, err
|
|
}
|
|
|
|
m.nonce = nonce
|
|
|
|
encryptedData := secretbox.Seal(nil, msg.toBytes(), &m.nonce, &engine.secretKey)
|
|
|
|
// assign the encrypted data to the message
|
|
m.data = encryptedData
|
|
|
|
// calculate the overall size of the message
|
|
m.length = uint64(len(m.data) + len(m.nonce) + 8)
|
|
|
|
return m, nil
|
|
|
|
}
|
|
|
|
// This method accepts the message as byte slice and the public key of the receiver of the messae,
|
|
// then encrypts it using the asymmetric key public key.
|
|
// If the public key is not privisioned and does not have the required length of 32 bytes it raises an exception.
|
|
func (engine *CryptoEngine) NewEncryptedMessageWithPubKey(msg message, verificationEngine VerificationEngine) (EncryptedMessage, error) {
|
|
|
|
encryptedMessage := EncryptedMessage{}
|
|
|
|
// get the peer public key
|
|
peerPublicKey := verificationEngine.PublicKey()
|
|
|
|
// check the size of the peerPublicKey
|
|
if len(peerPublicKey) != keySize {
|
|
return encryptedMessage, KeyNotValidError
|
|
}
|
|
|
|
// check the peerPublicKey is not empty (all zeros)
|
|
if bytes.Compare(peerPublicKey[:], emptyKey) == 0 {
|
|
return encryptedMessage, KeyNotValidError
|
|
}
|
|
|
|
// derive nonce
|
|
nonce, err := deriveNonce(engine.nonceKey, engine.salt, engine.context, engine.fetchAndIncrement())
|
|
if err != nil {
|
|
return encryptedMessage, err
|
|
}
|
|
|
|
// set the nonce to the encrypted message
|
|
encryptedMessage.nonce = nonce
|
|
|
|
// calculate the hash of the peer public key
|
|
sha224String := fmt.Sprintf("%x", sha256.Sum224(peerPublicKey[:]))
|
|
|
|
// lock the mutex
|
|
engine.mutex.Lock()
|
|
|
|
// check if the pre sgared key is already present in the map
|
|
if preSharedKey, ok := engine.preSharedKeysMap[sha224String]; ok { // means the key is there
|
|
// unlock the mutex
|
|
engine.mutex.Unlock()
|
|
|
|
// encrypt with the pre-computed key
|
|
encryptedData := box.SealAfterPrecomputation(nil, msg.toBytes(), &nonce, &preSharedKey)
|
|
|
|
// assign the encrypted data to the message
|
|
encryptedMessage.data = encryptedData
|
|
|
|
} else { // means the key is not there
|
|
|
|
// generate the key
|
|
// init the buffer
|
|
preSharedKey = [keySize]byte{}
|
|
|
|
// precompute the share key
|
|
box.Precompute(&preSharedKey, &peerPublicKey, &engine.privateKey)
|
|
|
|
// assign it to the map
|
|
engine.preSharedKeysMap[sha224String] = preSharedKey
|
|
|
|
// unlock the mutex once the map has the sharedKey set
|
|
engine.mutex.Unlock()
|
|
|
|
// encrypt with the pre-computed key
|
|
encryptedData := box.SealAfterPrecomputation(nil, msg.toBytes(), &nonce, &preSharedKey)
|
|
|
|
// assign the encrypted data to the message
|
|
encryptedMessage.data = encryptedData
|
|
}
|
|
|
|
// calculate the size of the message
|
|
encryptedMessage.length = uint64(len(encryptedMessage.data) + len(encryptedMessage.nonce) + 8)
|
|
|
|
return encryptedMessage, nil
|
|
|
|
}
|
|
|
|
// This method is used to decrypt messages where symmetrci encryption is used
|
|
func (engine *CryptoEngine) Decrypt(encryptedBytes []byte) (*message, error) {
|
|
|
|
var err error
|
|
msg := new(message)
|
|
|
|
// convert the bytes to an encrypted message
|
|
encryptedMessage, err := encryptedMessageFromBytes(encryptedBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
decryptedMessageBytes, valid := secretbox.Open(nil, encryptedMessage.data, &encryptedMessage.nonce, &engine.secretKey)
|
|
|
|
// if the verification failed
|
|
if !valid {
|
|
return nil, MessageDecryptionError
|
|
}
|
|
|
|
// means we successfully managed to decrypt
|
|
msg, err = messageFromBytes(decryptedMessageBytes)
|
|
return msg, nil
|
|
|
|
}
|
|
|
|
// This method is used to decrypt messages where symmetrci encryption is used
|
|
func (engine *CryptoEngine) DecryptWithPublicKey(encryptedBytes []byte, verificationEngine VerificationEngine) (*message, error) {
|
|
|
|
var err error
|
|
|
|
// get the peer public key
|
|
peerPublicKey := verificationEngine.PublicKey()
|
|
|
|
// convert the bytes to an encrypted message
|
|
encryptedMessage, err := encryptedMessageFromBytes(encryptedBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Make sure the key has a valid size
|
|
if len(peerPublicKey) < keySize {
|
|
return nil, KeyNotValidError
|
|
}
|
|
|
|
// calculate the hash of the peer public key
|
|
sha224String := fmt.Sprintf("%x", sha256.Sum224(peerPublicKey[:]))
|
|
|
|
// lock the mutex
|
|
engine.mutex.Lock()
|
|
|
|
// check if the pre sgared key is already present in the map
|
|
if preSharedKey, ok := engine.preSharedKeysMap[sha224String]; ok { // means the key is there
|
|
// unlock the mutex
|
|
engine.mutex.Unlock()
|
|
|
|
messageBytes, err := decryptWithPreShared(preSharedKey, encryptedMessage)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return messageFromBytes(messageBytes)
|
|
|
|
} else {
|
|
// otherwise decrypt with the standard box open function
|
|
messageBytes, valid := box.Open(nil, encryptedMessage.data, &encryptedMessage.nonce, &peerPublicKey, &engine.privateKey)
|
|
if !valid {
|
|
return nil, MessageDecryptionError
|
|
}
|
|
return messageFromBytes(messageBytes)
|
|
}
|
|
|
|
}
|
|
|
|
func decryptWithPreShared(preSharedKey [keySize]byte, m EncryptedMessage) ([]byte, error) {
|
|
if decryptedMessage, valid := box.OpenAfterPrecomputation(nil, m.data, &m.nonce, &preSharedKey); !valid {
|
|
return nil, MessageDecryptionError
|
|
} else {
|
|
return decryptedMessage, nil
|
|
}
|
|
}
|