End-to-end encryption in Go

Sung
Monomax Software
https://monomax.sh

Agenda

  1. Definition
  2. Example

1. Definition

A communication system where only the participants can read messages.

  • Only Alice and Bob can read the message
  • Need a shared secret (symmetric encryption)
  • Or use asymmetric encryption

What it's not

Typically messages pass through intermeidary and is stored by them.

  • Only encrypted in transit
  • Intermediary can read the message

2. Example

Symmetric Block Cipher

A block cipher that uses the same key to encrypt and decrypt

func AesGcmEncrypt(key, plaintext []byte) (string, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}

	aesgcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}

	nonce := make([]byte, aesGcmNonceSize)
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return "", err
	}

	ciphertext := aesgcm.Seal(nonce, nonce, []byte(plaintext), nil)
	ciphertextB64 := base64.StdEncoding.EncodeToString(ciphertext)

	return ciphertextB64, nil
}
func AesGcmDecrypt(key []byte, dataB64 string) ([]byte, error) {
	data, err := base64.StdEncoding.DecodeString(dataB64)
	if err != nil {
		return nil, err
	}

	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	aesgcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}

	nonce, ciphertext := data[:aesGcmNonceSize], data[aesGcmNonceSize:]
	plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
	if err != nil {
		return nil, err
	}

	return plaintext, nil
}

Designs

  • AES (Advanced Encryption Standard)
  • DES (Data Encryption Standard)

AES Encryption in Go

Encrypt

func AesGcmEncrypt(key, plaintext []byte) (string, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}

	aesgcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}

	nonce := make([]byte, aesGcmNonceSize)
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return "", err
	}

	ciphertext := aesgcm.Seal(nonce, nonce, []byte(plaintext), nil)
	ciphertextB64 := base64.StdEncoding.EncodeToString(ciphertext)

	return ciphertextB64, nil
}

Create a new cipher

block, err := aes.NewCipher(key)
if err != nil {
  return "", err
}

Key can be 16, 24, or 32 bytes
to select AES-128, AES-192, or AES-256

func AesGcmEncrypt(key, plaintext []byte) (string, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}

	aesgcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}

	nonce := make([]byte, aesGcmNonceSize)
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return "", err
	}

	ciphertext := aesgcm.Seal(nonce, nonce, []byte(plaintext), nil)
	ciphertextB64 := base64.StdEncoding.EncodeToString(ciphertext)

	return ciphertextB64, nil
}

Choose a mode of operation

aesgcm, err := cipher.NewGCM(block)
if err != nil {
  return "", err
} 

Mode of operation

How to repeatedly apply single-block operation to transform data bigger than a block

block ciphers encrypt only fixed-size blocks.

e.g. AES uses 128 bits block size.

Mode of operations

  • CBC (Cipher block chaining)
  • CTR (Counter)
  • GCM (Galois/Counter Mode)
  • ...

GCM

Provides both confidentiality and authenticity

func AesGcmEncrypt(key, plaintext []byte) (string, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}

	aesgcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}

	nonce := make([]byte, aesGcmNonceSize)
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return "", err
	}

	ciphertext := aesgcm.Seal(nonce, nonce, []byte(plaintext), nil)
	ciphertextB64 := base64.StdEncoding.EncodeToString(ciphertext)

	return ciphertextB64, nil
}

Encrypt using nonce

nonce := make([]byte, aesGcmNonceSize)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
  return "", err
}

ciphertext := aesgcm.Seal(nonce, nonce, []byte(plaintext), nil) 
  • Nonce is used to generate a stream of keys (k1,k2,...,kn)
  • keys are XORed with each block
func AesGcmEncrypt(key, plaintext []byte) (string, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}

	aesgcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}

	nonce := make([]byte, aesGcmNonceSize)
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return "", err
	}

	ciphertext := aesgcm.Seal(nonce, nonce, []byte(plaintext), nil)
	ciphertextB64 := base64.StdEncoding.EncodeToString(ciphertext)

	return ciphertextB64, nil
}

Base64 encode

ciphertextB64 := base64.StdEncoding.EncodeToString(ciphertext)

return ciphertextB64, nil 
func AesGcmEncrypt(key, plaintext []byte) (string, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}

	aesgcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}

	nonce := make([]byte, aesGcmNonceSize)
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return "", err
	}

	ciphertext := aesgcm.Seal(nonce, nonce, []byte(plaintext), nil)
	ciphertextB64 := base64.StdEncoding.EncodeToString(ciphertext)

	return ciphertextB64, nil
}

Decrypt

func AesGcmDecrypt(key []byte, dataB64 string) ([]byte, error) {
	data, err := base64.StdEncoding.DecodeString(dataB64)
	if err != nil {
		return nil, err
	}

	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	aesgcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}

	nonce, ciphertext := data[:aesGcmNonceSize], data[aesGcmNonceSize:]
	plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
	if err != nil {
		return nil, err
	}

	return plaintext, nil
}

Dnote

A simple end-to-end encrypted notebook for developers

A note is just a message to the future self.

Establishing a secret shared with the future self

Registering on dnote.io

  • User enters email and password
  • Derive k <- pbkdf2(password: password, salt: email, alg: 'sha256', iteration: 10000)
  • Derive enc_key <- random_256_bits()
  • Derive auth_key <- hkdf(alg: 'sha256', secret: k, salt: email, info: 'auth') (sent to server for authentication)
  • Derivce enc_key_enc <- aes256(secret: k, data: enc_key)
  • POST to '/register': (email, auth_key, enc_key_enc)
$ dnote login
  [?] email: sung@monomax.sh
  [?] password: 
func Do(ctx infra.DnoteCtx, email, password string) error {
	masterKey, authKey, err := crypt.MakeKeys([]byte(password), []byte(email))
	if err != nil {
		return err
	}

	authKeyB64 := base64.StdEncoding.EncodeToString(authKey)
	signinResp, err := client.Signin(ctx, email, authKeyB64)
	if err != nil {
		return err
	}

	cipherKeyDec, err := crypt.AesGcmDecrypt(masterKey, signinResp.CipherKeyEnc)
	if err != nil {
		return err
	}
	...
}

Free and open source

https://github.com/dnote/dnote

Alternatives

  • Asymmetric encryption (RSA, ElGamal)
  • Key exchange (Diffie-Hellman)

Summary

  • End-to-end encryption
  • Example using AES 256 symmetric block cipher

Attribution

  • User icon: https://www.iconfinder.com/icons/172621/female_user_icon (Creative Commons License)
  • User icon/server icon: Github Octicon (MIT License)

Links

Thanks

Sung
Monomax Software
sung@monomax.sh
https://monomax.sh