evalphobia/hierogolyph

View on GitHub
hierogolyph.go

Summary

Maintainability
A
50 mins
Test Coverage
package hierogolyph

import (
    "fmt"
    "strings"

    "github.com/evalphobia/hierogolyph/hasher"
    "github.com/evalphobia/hierogolyph/hsm"
)

// Hierogolyph treats encryption and decryption.
type Hierogolyph struct {
    Config

    Password      string
    Salt          string
    EncryptionKey string // generated by password and salt, used for encryption/decryption and verifying password.
}

// CreateHierogolyph creates new Hierogolyph from given password, which is used for encryption.
// (after the first encryption, don't use this constructor.)
func CreateHierogolyph(password string, conf Config) (Hierogolyph, error) {
    salt, err := getRandomString(20)
    if err != nil {
        return Hierogolyph{}, err
    }

    h := Hierogolyph{
        Config:   conf,
        Password: password,
        Salt:     salt,
    }
    err = h.SetEncryptionKey()
    if err != nil {
        return Hierogolyph{}, err
    }
    return h, nil
}

// SetEncryptionKey sets an encryption key generated from password and salt.
func (h *Hierogolyph) SetEncryptionKey() error {
    ek, err := h.createEncryptionKey()
    if err != nil {
        return err
    }

    h.EncryptionKey = ek
    return nil
}

// Unlock creates Content Encryption Key.
func (h Hierogolyph) Unlock() (cek string, err error) {
    z1, z2 := createDigests(h.Password, h.Salt, h.Config.Hasher)
    maskedCipherText, err := decodeBase64(h.EncryptionKey)
    if err != nil {
        return "", err
    }

    // get XOR between Z1 and R'
    encryptedSecretR := xor(maskedCipherText, z1)

    secretR, err := h.Config.HSM.Decrypt(encryptedSecretR)
    if err != nil {
        return "", err
    }

    return createCEK(z2, secretR), nil
}

// Encrypt encrypts given plainText.
func (h Hierogolyph) Encrypt(plainText string) (cipherText string, err error) {
    cek, err := h.Unlock()
    if err != nil {
        return "", err
    }

    fingerPrint := HashHMAC(plainText, h.Config.HMACKey)
    fingerprintedText := fmt.Sprintf("%s.%s", encodeBase64String(plainText), encodeBase64String(fingerPrint))

    cipherText, err = h.Config.Cipher.Encrypt(fingerprintedText, []byte(cek))
    if err != nil {
        return "", err
    }

    encrypted := encodeBase64String(cipherText)
    return fmt.Sprintf("%s.%s", encodeBase64String(h.EncryptionKey), encrypted), nil
}

// Decrypt decrypts given cipherText.
func (h Hierogolyph) Decrypt(cipherText string) (plainText string, err error) {
    encryptionKey, encryptedText, err := decodeCipherText(cipherText)
    if err != nil {
        return "", err
    }

    h.EncryptionKey = encryptionKey
    cek, err := h.Unlock()
    if err != nil {
        return "", err
    }

    fingerprintedText, err := h.Config.Cipher.Decrypt(encryptedText, []byte(cek))
    if err != nil {
        return "", err
    }

    parts := strings.Split(fingerprintedText, ".")
    if len(parts) != 2 {
        return "", fmt.Errorf("fingerprintedText=[%s] must have one dot `.`", fingerprintedText)
    }

    plainText, err = decodeBase64(parts[0])
    if err != nil {
        return "", err
    }
    fingerPrint, err := decodeBase64(parts[1])
    if err != nil {
        return "", err
    }

    expected := HashHMAC(plainText, h.Config.HMACKey)
    if fingerPrint != expected {
        return "", fmt.Errorf("HMAC finger print error: expected=[%s], actual=[%s]", expected, fingerPrint)
    }

    return plainText, nil
}

// createEncryptionKey creates encryption key from password and salt.
func (h *Hierogolyph) createEncryptionKey() (string, error) {
    secretR, err := getRandomBytes(32)
    if err != nil {
        return "", err
    }

    conf := h.Config
    z1, _ := createDigests(h.Password, h.Salt, conf.Hasher)
    return createEncryptionKey(z1, string(secretR), conf.HSM)
}

// decodeCipherText decodes from cipherText and returns encryptionKey and encryptedText.
func decodeCipherText(cipherText string) (encryptionKey, encryptedText string, err error) {
    parts := strings.Split(cipherText, ".")
    if len(parts) != 2 {
        return "", "", fmt.Errorf("cipherText=[%s] must have one dot `.`", cipherText)
    }

    encryptionKey, err = decodeBase64(parts[0])
    if err != nil {
        return "", "", err
    }
    encryptedText, err = decodeBase64(parts[1])
    if err != nil {
        return "", "", err
    }

    return encryptionKey, encryptedText, nil
}

// createDigests creates 32byte string pair from given password and salt by hashing.
func createDigests(password, salt string, hasher hasher.Hasher) (z1, z2 string) {
    digest := hasher.Hash(password, salt)
    return digest[0:32], digest[32:64]
}

// createEncryptionKey creates EncryptionKey from Z1 and R with HSM eryption.
func createEncryptionKey(z1, secretR string, hsm hsm.HSM) (encryptionKey string, err error) {
    encryptedSecretR, err := hsm.Encrypt(secretR)
    if err != nil {
        return "", err
    }

    // get XOR between Z1 and R'
    maskedCipherText := xor(string(encryptedSecretR), z1)
    return encodeBase64(maskedCipherText), nil
}

// createCEK returns Content Encryption Key from Z2 and R.
func createCEK(z2, secretR string) (cek string) {
    return HashSHA256(z2 + secretR)
}

// xor gets XOR bytes between 'a' and 'b'.
// The results is based on 'a's length.
// If 'a' is longer than 'b', 'b' will be padded by 0.
func xor(a, b string) []byte {
    byteSize := len(a)
    paddedB := paddingLeft(string(b), byteSize, "0")
    result := make([]byte, byteSize)
    for i := 0; i < byteSize; i++ {
        result[i] = paddedB[i] ^ a[i]
    }
    return result
}