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. $ 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.
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.
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!