How to Combine two Programming Languages: A Ruby and Golang Tutorial

Written by bunt | Published 2022/01/20
Tech Story Tags: ruby | ruby-on-rails | golang | learn-to-code-ruby | learn-to-golang | security | programming | software-development

TLDRDuring past projects, I experienced difficulties when developing with a Ruby encryption pack. Specifically, the project in question was a payment gateway and integration project for an Asian bank. They set up security requirements, one of which was the encryption of requests using 3DES-ECB. In the process of searching, I did not find a suitable Ruby library that would help me to create such a query. Сrypto pack is included in the basic Go library, but here 3DES was only in CBC mode. Then I had to resort to a non-standard solution, combining both languages. via the TL;DR App

During past projects, I experienced difficulties when developing with a Ruby encryption pack.
Specifically, the project in question was a payment gateway and integration project for an Asian bank. They set up security requirements, one of which was the encryption of requests using 3DES-ECB.
In the process of searching, I did not find a suitable Ruby library that would help me to create such a query.
Сrypto pack is included in the basic Go library, but here 3DES was only in CBC mode. Then I had to resort to a non-standard solution, combining both languages. 
The Main Differences Between the ECB and the CBC
Electronic codebook (ECB):  In this encryption mode, the message is divided into blocks and each block is encrypted separately.
The disadvantage of this system is that if your message contains 2 identical blocks, they will have the same encrypted output.
This, unfortunately, does not hide data patterns well. Therefore, this method is not recommended for use in cryptographic protocols (Menezes, Alfred J.; van Oorschot, Paul C.; Vanstone, Scott A. (2018). Handbook of Applied Cryptography, p. 228.). 
Today, this method is vulnerable and lost its relevance. This method is good for those who want to know basic cryptographic algorithms. 
Cipher Block Chaining (CBC) was invented in 1976. In CBC mode, each plaintext block is XORed with the previous ciphertext block before encryption.
Thus, each block of ciphertext depends on all blocks of plaintext processed up to that point. To make each message unique, an initialization vector should be used in the first block. 
Let's start writing the code. 
First, let's write a test: 
Let's create an acceptance test. Given:  encryption key and text.

First, we encrypt the text using the
tripleDesECBEncrypt 
method and compare it to the decrypted text using the second
tripleDesECBDecrypt
method. 
//TripleDES
// ./main_test.go


package main

import (
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestDesEncrypt(t *testing.T) {
	key := []byte("123456789012345678901234")
	origtext := []byte("VASIA MOSHN120049949123213")

	encryptedText, err := tripleDesECBEncrypt(origtext, key)
	if err != nil {
		t.Fatal(err)
	}
	fmt.Printf("%v\n", encryptedText) // Let's see what the text turns into 
	decrypted_text, error := tripleDesECBDecrypt([]byte(encryptedText), key)
	if error != nil {
		t.Fatal(error)
	}
	assert.Equal(t, string(origtext), string(decrypted_text))
}
To make the test work, we throw in the functions:
//TripleDES
// ./main.go

package main

func main() {

}

func tripleDesECBEncrypt(src, key []byte) (string, error) {
	return "encrypt", nil

}

func tripleDesECBDecrypt(src, key []byte) (string, error) {
	return "decrypt", nil

}
Initialize Dependency Management:
$ go mod init tripleDES
This command initializes and writes a new
go.mod
file in the current directory, effectively creating a new module rooted in the current directory.

Next, you need to add dependencies to the module and sums: 
$ go mod tidy
It adds any missing requirements to modules required to build packages and dependencies of the current module, and removes dependencies that are not used.
Moreover, it includes all missing entries in
go.sum 
and removes unnecessary ones. Also, this command is used to add dependencies, assembly combinations for different OS, architectures and tags.
Editor's note regarding the above image: I have so many questions. 
Run test:
$ go test
Encrypt
--- FAIL: TestDesEncrypt (0.00s)
    main_test.go:23: 
                Error Trace:    main_test.go:23
                Error:          Not equal: 
                                expected: "VASIA MOSHN120049949123213"
                                actual  : "Decrypt"
                            
                                Diff:
                                --- Expected
                                +++ Actual
                                @@ -1 +1 @@
                                -VASIA MOSHN120049949123213
                                +Decrypt
                Test:           TestDesEncrypt
FAIL
exit status 1
FAIL    tripleDES       0.350s
How
tripleDES 
Works: 
DES and tripleDES uses the block length for encryption of 8 bytes or 64 bits. 
The text can be of any length and we need to add it so that we can divide it into blocks of 8 bytes. 
For this purpose, use PKCS5 Padding.
The scheme is simple: Based on the remainder of the division, find out how many bytes are missing for an 8-byte division into blocks and include the so-called padding.

 Example: 

Let's assume that we have text
VASIA MOSHN1200499491232133. 
The length of this text is 27 bytes. We need to get the remainder of the division 27% 8 = 3.
To understand how many bytes are missing, we need to subtract the result of the remainder from 8 bytes 8 - 3 = 5. That is, the required insert is 5 bytes. 
Let's create 5 byte text, [5,5,5,5,5] and add it to the end of the text. Let's write the same in the form of code:
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
	padding := blockSize - (len(ciphertext)%blockSize) // Let's find out how many bytes are still missing.
	// padding := 8-(27%8) = 8-3 = 5
	padtext := bytes.Repeat([]byte{byte(padding)}, padding) // Let's create text for the number of bytes that is not enough to divide by 8
	// padtext := []byte{5,5,5,5,5}
	return append(ciphertext, padtext...) // Add it to the end of the text
}
Surely, some readers will wonder why we are filling with the insert value, and not just creating a slice with zero or other values. The answer is simple, to get back the readable text, we need to know how many elements should be removed from the slice.
Let's create this algorithm: 
func PKCS5UnPadding(origData []byte) []byte {
   length := len(origData) // Get the length of the slice
   unpadding := int(origData[length-1]) //Get the last element of the slice. It will tell us how many elements were added to the slice
   return origData[:(length - unpadding)] // Returning a slice without extra elements

}
We will write the code in order to encrypt a new line: 
func tripleDesECBEncrypt(src, key []byte) (string, error) {
	block, err := des.NewTripleDESCipher(key) // this divides our key into three parts of 8 bytes and creates subkeys from these, so if your key is not equal to 24 bytes, you will not succeed
	if err != nil {
		return "", err
	}
	bs := block.BlockSize() // Find out what block size
	origData := PKCS5Padding(src, bs) // Let's finalize our text so that it becomes valid for this encryption method
	if len(origData)%bs != 0 {
		return nil, errors.New("Need a multiple of the blocksize")
	}


	out := make([]byte, len(origData)) // Let's create a slice to fill in encrypted data
	dst := out // We use this construction instead append

	for len(origData) > 0 {
		block.Encrypt(dst, origData[:bs]) // func Encrypt encrypts the origData block[:bs] in dst.
		origData = origData[bs:] // Decrease string length by block size
		dst = dst[bs:] // Decrease slice reference length by block size
	}

	// Example with append
		// var out []byte
		// dst := make([]byte, bs)
		// iterationSteps := len(origData) / bs
		// for i := 0; i < iterationSteps; i++ {
		//   block.Encrypt(dst, origData[:bs])
		//   origData = origData[bs:]
		//   out = append(out, dst...)
		// }

	return out, nil
}
For the test, we will decrypt the text:
func tripleDesECBDecrypt(src, key []byte) (string, error) {
	block, err := des.NewTripleDESCipher(key) // this divides our key into three parts of 8 bytes and creates subkeys from these, so if your key is not equal to 24 bytes, you will not succeed 
	if err != nil {
		return nil, err
	}
	bs := block.BlockSize() // Find out what block size
	if len(src)%bs != 0 {
		return nil, errors.New("crypto/cipher: input not full blocks")
	}
	out := make([]byte, len(src)) // Make a slice to fill in the decrypted data
	dst := out // use this construction instead append
	for len(src) > 0 {
		block.Decrypt(dst, src[:bs]) // Decrypt decrypts the origData[:bs] block into dst.
		src = src[bs:] // Decrease the string length by the block size
		dst = dst[bs:] // Decrease slice length by block size
	}
	out = PKCS5UnPadding(out) // Remove extra characters
	return out, nil
}
We will use a binary file with parameters to call it from Ruby class. Let's prepare a file to build a binary file, using the flag pack, in order to easily transfer parameters to the executable file. 
//TripleDES
// ./main.go
package main

import (
	"bytes"
	"crypto/des"
	"errors"
	"flag"
	"fmt"
)

func main() {
	textForParse := flag.String("text", "", "Text for parsing")
	key := flag.String("key", "", "key for encrypt/decrypt")
	decrypt := flag.Bool("d", false, "Value for decrypt")
	flag.Parse()
	var result []byte
	var err error
	if *decrypt {
		result, err = tripleDesECBDecrypt([]byte(*textForParse), []byte(*key))
	} else {
		result, err = tripleDesECBEncrypt([]byte(*textForParse), []byte(*key))
	}
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(string(result))
	}
}
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
	padding := blockSize - (len(ciphertext) % blockSize) // Find out how many bytes are still missing
	// padding := 8-(27%8) = 8-3 = 5
	padtext := bytes.Repeat([]byte{byte(padding)}, padding) // Let's create text for the number of bytes that is not enough to divide by 8
	// padtext := []byte{5,5,5,5,5}
	return append(ciphertext, padtext...) // Add it to the end of the text
}
func PKCS5UnPadding(origData []byte) []byte {
	length := len(origData)                // Add it to the end of the text
	unpadding := int(origData[length-1])   // Get the last element of the slice.  It will tell us how many elements were added to the slice
	return origData[:(length - unpadding)] // Returning a slice without extra elements
}
func tripleDesECBEncrypt(src, key []byte) (string, error) {
	block, err := des.NewTripleDESCipher(key) // Divides the key into three parts of 8 bytes and creates subkeys from these, so if your key is not equal to 24 bytes, you will not succeed
	if err != nil {
		return nil, err
	}
	bs := block.BlockSize()           // Let’s find out what block size
	origData := PKCS5Padding(src, bs) // Let's finalize our text so that it becomes valid for this encryption method
	if len(origData)%bs != 0 {
		return nil, errors.New("need a multiple of the blocksize")
	}
	out := make([]byte, len(origData)) // Let's create a slice to fill in encrypted data
	dst := out                         // We use this construction instead of append
	for len(origData) > 0 {
		block.Encrypt(dst, origData[:bs]) //Decrypt decrypts the block origData[:bs] в dst.
		origData = origData[bs:]          // Decrease the string length by the block size
		dst = dst[bs:]                    // Decrease slice reference length by block size
	}
	Variant
	with
	append
	// var out []byte
	// dst := make([]byte, bs)
	// iterationSteps := len(origData) / bs
	// for i := 0; i < iterationSteps; i++ {
	//   block.Encrypt(dst, origData[:bs])
	//   origData = origData[bs:]
	//   out = append(out, dst...)
	// }
	return out, nil
}
func tripleDesECBDecrypt(src, key []byte) (string, error) {
	block, err := des.NewTripleDESCipher(key) // we divide the key into three parts of 8 bytes each and create subkeys from these, so if your key is not equal to 24 bytes, you will not succeed
	if err != nil {
		return nil, err
	}
	bs := block.BlockSize() // Find out what block size
	if len(src)%bs != 0 {
		return nil, errors.New("crypto/cipher: input not full blocks")
	}
	out := make([]byte, len(src)) // Let's create a slice to fill in the decrypted data
	dst := out                    // We use this construction instead of append
	for len(src) > 0 {
		block.Decrypt(dst, src[:bs]) // Decrypt decrypts the block origData[:bs] в dst.
		src = src[bs:]               // Decrease the string length by the block size
		dst = dst[bs:]               // Decrease slice length by block size
	}
	out = PKCS5UnPadding(out) // Removing extra characters
	return out, nil
}
Let's create a binary file:
$ go build
the triple-des file will appear in the root, you need to check it for work:
$ ./triple-des -key=123456789012345678901234 -text="VASIA MOSHN1200499491232133"

# the answer will be an encrypted string 
# eb75f543d000bf93c8c634b5c638bc4b31912b30c899efd492102d95abfd4c84
Now let's decrypt the string:
$ ./triple-des -key=123456789012345678901234 -text=eb75f543d000bf93c8c634b5c638bc4b31912b30c899efd492102d95abfd4c84 -d

#VASIA MOSHN1200499491232133
The answer is the same as what we encrypted earlier.

Now let's make it possible for the binary to run with ruby:

And again, to begin with, we write tests:
RSpec.describe TripleDes::Encrypt do
  subject(:service) do
    described_class.new(src, key).call
  end

  let(:src) { 'VASIA MOSHN1200499491232133' }
  let(:key) { '123456789012345678901234' }

  context 'when text encrypted' do
    it 'return base64 string' do
      expect(service).to eql('eb75f543d000bf93c8c634b5c638bc4b31912b30c899efd492102d95abfd4c84')
    end
  end

  context 'when decode string' do
    let(:src) { 'eb75f543d000bf93c8c634b5c638bc4b31912b30c899efd492102d95abfd4c84' }

    let(:service) { `./triple-des -text='#{src}' -key='#{key}' -d` }

    it 'return decode string' do
      expect(service).to eql("VASIA MOSHN1200499491232133")
    end
  end
end
The Class Itself:
module TripleDes
  class Encrypt
    def initialize(src, key)
      @src = src
      @key = key
    end

    def call
      io = IO.popen(['./triple-des', "-text=#{src}", "-key=#{key}"])

      res = io.read.strip
      io.close
      res
    end

    private

    attr_reader: src, :key
  end
end
Let's check how it works:
# frozen_string_literal: true

require 'rspec/autorun'

module TripleDes
  class Encrypt
    def initialize(src, key)
      @src = src
      @key = key
    end

    def call
      io = IO.popen(['./triple-des', "-text=#{src}", "-key=#{key}"])

      res = io.read.strip
      io.close
      res
    end

    private

    attr_reader :src, :key
  end
end

RSpec.describe TripleDes::Encrypt do
  subject(:service) do
    described_class.new(src, key).call
  end

  let(:src) { 'VASIA MOSHN1200499491232133' }
  let(:key) { '123456789012345678901234' }

  context 'when text encrypted' do
    it 'return base64 string' do
      expect(service).to eql('eb75f543d000bf93c8c634b5c638bc4b31912b30c899efd492102d95abfd4c84')
    end
  end

  context 'when decode string' do
    let(:src) { 'eb75f543d000bf93c8c634b5c638bc4b31912b30c899efd492102d95abfd4c84' }

    let(:service) { `./triple-des -text='#{src}' -key='#{key}' -d` }

    it 'return decode string' do
      expect(service).to eql("VASIA MOSHN1200499491232133\n")
    end
  end
end

$ ruby ./triple-des.rb
..

Finished in 0.01517 seconds (files took 0.08794 seconds to load)
2 examples, 0 failures
The repository with the code can be found here: https://github.com/sabunt/triple_des
Conclusion
The need to use the 3DES algorithm was determined by the requirements of the bank in which I worked at that time.
There is an opinion that just because of the block size of 64 bits, this method is not safe and there are many ways to crack it. I think this is why this encryption method is not included in the standard libraries in Ruby and Go.
To encrypt your data, I recommend using the AES algorithm, the block length of which is all 128 bits.
To solve my problem, I had to use a non-standard method and combine the two languages. Such symbiosis is possible in other programming languages as well. Get creative and achieve your goals! Good luck!

Written by bunt | Lead Software Engineer at 3commas.io. Also developing efficient web applications, bots, etc. I love Ruby, Golang, Vue.js
Published by HackerNoon on 2022/01/20