• I’ve recently participated in a project of management among nodes in a cluster, it’s developed in golang.
  • My part is to develop a package to ensure those messages of communication between schedulers and executors are well encrypted, only receivers can decrypt them.
  • All codes in this article are permitted by other members in the project team to share.

  • Idea: This module both utilize rsa public key/private key and symmetric key.

    • symmetric key: generate a random symmetric key, and use the same key to encrypt and decrypt. In this module, we use package chacha20poly1305(https://godoc.org/golang.org/x/crypto/chacha20poly1305) and “crypto/rand” to finish this step. The symmetric key here is to encrypt/decrypt those message that nodes attempt to send or receive.
    • rsa keypairs: generate a pair of rsa keys as public key and private key to encrypt and decrypt. In this module, we first generate a random rsa keypair strings in keys.go, and use functions in crypting.go to handle those strings. The rsa keypairs here are to encrypt/decrypt the same symmetric key above to keep that key from being cracked by unknown hackers.
      • The sender and receiver are corresponding to “scheduler” or “executor” in the project, so that there are 2 pairs of rsa keys for both characters.

1. generate key pairs

  • prepare 2 pairs (as scheduler or executor) of private/public keys as generateKeyPairs.go: ```go package main

import ( “fmt”

  1. "ire.com/clustershell/crypting"

)

const ( // variable length of generated asymmetric keys(128, 256, 512, 2048…) asymmetricKeySize = 1024 )

func main() { fmt.Println(“package keypairs\n”)

  1. pvtkeyScheduler, pubkeyScheduler := crypting.GenerateKeyPair(asymmetricKeySize)
  2. pvtkeyExecutor, pubkeyExecutor := crypting.GenerateKeyPair(asymmetricKeySize)
  3. pvtbyteScheduler := crypting.PrivateKeyToBytes(pvtkeyScheduler)
  4. pubbyteScheduler := crypting.PublicKeyToBytes(pubkeyScheduler)
  5. pvtbyteExecutor := crypting.PrivateKeyToBytes(pvtkeyExecutor)
  6. pubbyteExecutor := crypting.PublicKeyToBytes(pubkeyExecutor)
  7. fmt.Printf("var PvtkeyScheduler = `%s`\n", string(pvtbyteScheduler))
  8. fmt.Printf("var PubkeyScheduler = `%s`\n", string(pubbyteScheduler))
  9. fmt.Printf("var PvtkeyExecutor = `%s`\n", string(pvtbyteExecutor))
  10. fmt.Printf("var PubkeyExecutor = `%s`\n", string(pubbyteExecutor))

}

  1. - In Makefile, add these lines to redirect generateKeyPairs.go and save key pairs in keys.go:
  2. ```go
  3. all:
  4. @$(MAKE) --no-print-directory generatekeypairs
  5. //...
  6. .PHONY: generatekeypairs
  7. generatekeypairs:
  8. go run ./cmd/keygen/generateKeyPairs.go > ./crypting/keypairs/keys.go
  • 2 pairs of keys are all strings

2. crypting.go update

  • Because 2 pairs of keys in keys.go are strings instead of rsa.PrivateKey or rsa.PublicKey, here are two new functions to handle encrypting/decrypting in crypting.go: ```go // Encrypt with public key string instead of rsa.PublicKey func Encrypt(unencrypted []byte, pubkeystr string) ([]byte, error) { pubkey := BytesToPublicKey([]byte(pubkeystr))

    hash := sha1.New() ciphertext, err := rsa.EncryptOAEP(hash, rand.Reader, pubkey, unencrypted, nil) if err != nil {

    1. logger.Error(err)

    }

    return ciphertext, err }

// Decrypt with private key string instead of rsa.PrivateKey func Decrypt(encrypted []byte, pvtkeystr string) ([]byte, error) { pvtkey := BytesToPrivateKey([]byte(pvtkeystr))

  1. hash := sha1.New()
  2. plaintext, err := rsa.DecryptOAEP(hash, rand.Reader, pvtkey, encrypted, nil)
  3. if err != nil {
  4. logger.Error(err)
  5. }
  6. return plaintext, err

}

  1. ---
  2. <a name="to6ip"></a>
  3. ### Test file (only use rsa keypairs):
  4. ```go
  5. package main
  6. import (
  7. "fmt"
  8. "ire.com/clustershell/crypting"
  9. "ire.com/clustershell/crypting/keypairs"
  10. )
  11. func main() {
  12. fmt.Println("\n", "kengen_crypt", "\n")
  13. pvtkeystrScheduler := keypairs.PvtkeyScheduler
  14. pubkeystrScheduler := keypairs.PubkeyScheduler
  15. fmt.Printf("Scheduler_private_key_byte_str\n%v", pvtkeystrScheduler)
  16. fmt.Printf("Scheduler_public_key_byte_str\n%v", pubkeystrScheduler)
  17. pvtkeystrExecutor := keypairs.PvtkeyExecutor
  18. pubkeystrExecutor := keypairs.PubkeyExecutor
  19. fmt.Printf("Executor_private_key_byte_str\n%v", pvtkeystrScheduler)
  20. fmt.Printf("Executor_public_key_byte_str\n%v", pubkeystrScheduler)
  21. msgScheduler := []byte("Hello World scheduler")Str
  22. encryptedScheduler, _ := crypting.EncryptWithPublicKeyStr(msgScheduler, pubkeystrScheduler)
  23. fmt.Println(string(encryptedScheduler))
  24. unencryptedScheduler, _ := crypting.DecryptWithPrivateKeyStr(encryptedScheduler, pvtkeystrScheduler)
  25. fmt.Println(string(unencryptedScheduler))
  26. msgExecutor := []byte("Hello World executor")
  27. encryptedExecutor, _ := crypting.EncryptWithPublicKeyStr(msgExecutor, pubkeystrExecutor)
  28. fmt.Println(string(encryptedExecutor))
  29. unencryptedExecutor, _ := crypting.DecryptWithPrivateKeyStr(encryptedExecutor, pvtkeystrExecutor)
  30. fmt.Println(string(unencryptedExecutor))
  31. }

3. Extension of symmetric key

  • Due to the situation that only pass with rsa keypairs cannot pass long messages (for example, keypairs with length of 4096 bytes can pass messages of 470 bytes), we use packs of symmetric keys instead.
  • In specific, we apply golang package chacha20poly1305
  • First, we have to update crypting.go for new packs and constants ```go import ( “crypto/rand” “crypto/rsa” “crypto/sha1” “crypto/x509” “encoding/pem”

    “ire.com/clustershell/logger”

    cryptorand “crypto/rand”

    chacha “golang.org/x/crypto/chacha20poly1305” )

const ( // KeySize is the size of the key used by this AEAD, in bytes. KeySize = 32

  1. // NonceSize is the size of the nonce used with the standard variant of this
  2. // AEAD, in bytes.
  3. NonceSize = 12
  4. // NonceSizeX is the size of the nonce used with the XChaCha20-Poly1305
  5. // variant of this AEAD, in bytes.
  6. NonceSizeX = 24

)

  1. - The idea of symmetric key is to pass messages and encrypt/decrypt with a generated symmetric key, while we encrypt/decrypt the same symmetric key with rsa keypairs.
  2. - With packs above, here is the set of new functions in crypting.go to do symmetric key crypting:
  3. ```go
  4. // Encrypt with public key string instead of rsa.PublicKey, finally return ciphertext and error
  5. func EncryptWithPublicKeyStr(unencrypted []byte, pubkeystr string) ([]byte, error) {
  6. pubkey := BytesToPublicKey([]byte(pubkeystr))
  7. hash := sha1.New()
  8. ciphertext, err := rsa.EncryptOAEP(hash, rand.Reader, pubkey, unencrypted, nil)
  9. if err != nil {
  10. logger.Error(err)
  11. }
  12. return ciphertext, err
  13. }
  14. // Decrypt with private key string instead of rsa.PrivateKey, finally return plaintext and error
  15. func DecryptWithPrivateKeyStr(encrypted []byte, pvtkeystr string) ([]byte, error) {
  16. pvtkey := BytesToPrivateKey([]byte(pvtkeystr))
  17. hash := sha1.New()
  18. plaintext, err := rsa.DecryptOAEP(hash, rand.Reader, pvtkey, encrypted, nil)
  19. if err != nil {
  20. logger.Error(err)
  21. }
  22. return plaintext, err
  23. }
  24. // SymmKeyGen generates new symmetric key for sender
  25. func SymmKeyGen(length int) []byte {
  26. key := make([]byte, KeySize)
  27. if _, err := cryptorand.Read(key); err != nil {
  28. panic(err)
  29. }
  30. return key
  31. }
  32. // EncryptMsg - encryption with symmetric key, and use rsa public key string to hide that symmetric key
  33. func EncryptMsg(msg []byte, pubkeystr string) ([]byte, []byte) {
  34. keySymm := SymmKeyGen(KeySize) //generate a random symmetric key as sender
  35. aead, err := chacha.NewX([]byte(keySymm)) //both sender and receiver do this step
  36. if err != nil {
  37. panic(err)
  38. }
  39. nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(msg)+aead.Overhead())
  40. if _, err := cryptorand.Read(nonce); err != nil {
  41. panic(err)
  42. }
  43. encryptedMsg := aead.Seal(nonce, nonce, msg, nil) //encrypt message with symmetric key
  44. encryptedKey, err := EncryptWithPublicKeyStr(keySymm, pubkeystr) //encrypt symmetric key with provided public key
  45. if err != nil {
  46. panic(err)
  47. }
  48. return encryptedMsg, encryptedKey
  49. }
  50. // DecryptMsg - symmetric decryption with symmetric key, while symmetric key is decrypted wih rsa private key string
  51. func DecryptMsg(encryptedMsg []byte, encryptedKey []byte, pvtkeystr string) ([]byte, error) {
  52. keySymm, err := DecryptWithPrivateKeyStr(encryptedKey, pvtkeystr) //decrypt symmetric key with provided private key
  53. if err != nil {
  54. logger.Error(err)
  55. }
  56. aead, err := chacha.NewX([]byte(keySymm)) //both sender and receiver do this step
  57. if err != nil {
  58. panic(err)
  59. }
  60. if len(encryptedMsg) < aead.NonceSize() {
  61. panic("ciphertext too short")
  62. }
  63. nonce, ciphertext := encryptedMsg[:aead.NonceSize()], encryptedMsg[aead.NonceSize():]
  64. plaintext, err := aead.Open(nil, nonce, ciphertext, nil) //decrypt message with symmetric key
  65. if err != nil {
  66. panic(err)
  67. }
  68. return plaintext, err
  69. }

Test file (use both rsa keypairs and symmetric keys)

  1. package main
  2. import (
  3. "fmt"
  4. "ire.com/clustershell/crypting"
  5. "ire.com/clustershell/crypting/keypairs"
  6. "ire.com/clustershell/logger"
  7. cryptorand "crypto/rand"
  8. )
  9. // Test - pick up key pairs from keys.go
  10. type Test struct {
  11. pvtkeystrScheduler string
  12. pubkeystrScheduler string
  13. pvtkeystrExecutor string
  14. pubkeystrExecutor string
  15. }
  16. var teststrings = `HelloWorld`
  17. //rsa logic: length of passing is in crypting.GenerateKeyPair(asymmetricKeySize) from generateKeyPairs.go
  18. func (t *Test) cryptingSimulationBoth(msg string) bool {
  19. fmt.Println("message length: ", len(msg))
  20. msgScheduler := msg
  21. //fmt.Println("Scheduler: ", cryptingSimulation(msgScheduler, t.pubkeystrScheduler, t.pvtkeystrScheduler))
  22. msgExecutor := msg
  23. //fmt.Println("Executor: ", cryptingSimulation(msgExecutor, t.pubkeystrExecutor, t.pvtkeystrExecutor), "\n")
  24. //return cryptingSimulation(msgScheduler, t.pubkeystrScheduler, t.pvtkeystrScheduler) && cryptingSimulation(msgExecutor, t.pubkeystrExecutor, t.pvtkeystrExecutor)
  25. return cryptingSimulationSymm(msgScheduler, t.pubkeystrScheduler, t.pvtkeystrScheduler) && cryptingSimulationSymm(msgExecutor, t.pubkeystrExecutor, t.pvtkeystrExecutor)
  26. }
  27. //crypting with only rsa keypairs
  28. func cryptingSimulation(msg string, pubkeystr string, pvtkeystr string) bool {
  29. encrypted, err := crypting.EncryptWithPublicKeyStr([]byte(msg), pubkeystr)
  30. //fmt.Println(string(encrypted))
  31. if err != nil {
  32. logger.Error(err)
  33. }
  34. unencrypted, err := crypting.DecryptWithPrivateKeyStr(encrypted, pvtkeystr)
  35. //fmt.Println(string(unencrypted))
  36. if err != nil {
  37. logger.Error(err)
  38. }
  39. return string(unencrypted) == msg
  40. }
  41. //crypting with both rsa keypairs and symmetric keys
  42. func cryptingSimulationSymm(msg string, pubkeystr string, pvtkeystr string) bool {
  43. //pubkeystr and pvtkeystr here are rsa keys
  44. //pubkeystr is used to encrypt the symmetric key into encryptedKey
  45. encryptedMsg, encryptedKey := crypting.EncryptMsg([]byte(msg), pubkeystr)
  46. //remember to pass encryptedKey and pvtkeystr to get the symmetric key
  47. unencrypted, err := crypting.DecryptMsg(encryptedMsg, encryptedKey, pvtkeystr)
  48. if err != nil {
  49. logger.Error(err)
  50. }
  51. return string(unencrypted) == msg
  52. }
  53. func main() {
  54. fmt.Println("\n", "kengen_crypt", "\n")
  55. //fmt.Printf("Scheduler_private_key_byte_str\n%v", pvtkeystrScheduler)
  56. //fmt.Printf("Scheduler_public_key_byte_str\n%v", pubkeystrScheduler)
  57. //fmt.Printf("Executor_private_key_byte_str\n%v", pvtkeystrScheduler)
  58. //fmt.Printf("Executor_public_key_byte_str\n%v", pubkeystrScheduler)
  59. //get 2 keypairs in keys.go
  60. test := Test{keypairs.PvtkeyScheduler, keypairs.PubkeyScheduler, keypairs.PvtkeyExecutor, keypairs.PubkeyExecutor}
  61. //add 1000 bytes every time
  62. str := make([]byte, 1000)
  63. cryptorand.Read(str)
  64. for { //this loop can show the maximum length of the message passes
  65. teststrings = teststrings + string(str)
  66. if !test.cryptingSimulationBoth(teststrings) {
  67. break
  68. }
  69. }
  70. }

Function in crypting.go:

  • GenerateKeyPair(int) (rsa.PrivateKey, rsa.PublicKey): return rsa privatekey and publickey according to provided length
  • PrivateKeyToBytes(*rsa.PrivateKey) []byte: convert rsa.PrivateKey to keybytes
  • PublicKeyToBytes(*rsa.PublicKey) []byte: convert rsa.PublicKey to keybytes
  • BytesToPrivateKey([]byte) *rsa.PrivateKey: convert keybytes to rsa.PrivateKey
  • BytesToPublicKey([]byte) *rsa.PublicKey: convert keybytes to rsa.PublicKey
  • EncryptWithPublicKey([]byte, *rsa.PublicKey) []byte: encrypt message with rsa public key, return ciphertext
  • DecryptWithPrivateKey([]byte, *rsa.PrivateKey) []byte: decrypt message with rsa private key, return plaintext
  • EncryptWithPublicKeyStr([]byte, string) ([]byte, error): encrypt message with rsa public key string, return ciphertext and error
  • DecryptWithPrivateKeyStr([]byte, string) ([]byte, error): encrypt message with rsa private key string, return plaintext and error
  • SymmKeyGen(int) []byte: return symmetric key according to provided length
  • EncryptMsg([]byte, string) ([]byte, []byte): encrypt message with randomly generated symmetric key, return encrypted message bytes and encrypted symmetric key that encrypted by provided rsa public key
  • DecryptMsg([]byte, []byte, string) ([]byte, error): decrypt symmetric key with provided rsa private key, and use this symmetric key to decrypt encrypted message, return plaintext and error