By default, GoLang doesn't provide the ECB mode cipher for DESede though there is CBC mode provided. In cases we need to encrypt/decrypt data with ECB mode, we need to implement those by ourselves. This mode is frequently used when encrypting/decrypting PIN block which is small block data less than 16 bytes.
In this post, we will introduce how to implement the DESede/ECB/NoPadding algorithm in GoLang by using the existing cipher support. Here we will not cover how DESede works in detail, instead we will just cover the logic to make it work.
Below is the sample code which can be used to encrypt/decrypt data in DESede(3DES) ECB mode.
import (
"bytes"
"crypto/des"
"errors"
)
// DESedeECBEncrypt ...
func DESedeECBEncrypt(origData, key []byte) ([]byte, error) {
tkey := make([]byte, 24, 24)
copy(tkey, key)
k1 := tkey[:8]
k2 := tkey[8:16]
k3 := tkey[16:]
buf1, err := encrypt(origData, k1)
if err != nil {
return nil, err
}
buf2, err := decrypt(buf1, k2)
if err != nil {
return nil, err
}
out, err := encrypt(buf2, k3)
if err != nil {
return nil, err
}
return out, nil
}
// DESedeECBDecrypt ...
func DESedeECBDecrypt(crypted, key []byte) ([]byte, error) {
tkey := make([]byte, 24, 24)
copy(tkey, key)
k1 := tkey[:8]
k2 := tkey[8:16]
k3 := tkey[16:]
buf1, err := decrypt(crypted, k3)
if err != nil {
return nil, err
}
buf2, err := encrypt(buf1, k2)
if err != nil {
return nil, err
}
out, err := decrypt(buf2, k1)
if err != nil {
return nil, err
}
return out, nil
}
func encrypt(origData, key []byte) ([]byte, error) {
if len(origData) < 1 || len(key) < 1 {
return nil, errors.New("wrong data or key")
}
block, err := des.NewCipher(key)
if err != nil {
return nil, err
}
bs := block.BlockSize()
if len(origData)%bs != 0 {
return nil, errors.New("wrong padding")
}
out := make([]byte, len(origData))
dst := out
for len(origData) > 0 {
block.Encrypt(dst, origData[:bs])
origData = origData[bs:]
dst = dst[bs:]
}
return out, nil
}
func decrypt(crypted, key []byte) ([]byte, error) {
if len(crypted) < 1 || len(key) < 1 {
return nil, errors.New("wrong data or key")
}
block, err := des.NewCipher(key)
if err != nil {
return nil, err
}
out := make([]byte, len(crypted))
dst := out
bs := block.BlockSize()
if len(crypted)%bs != 0 {
return nil, errors.New("wrong crypted size")
}
for len(crypted) > 0 {
block.Decrypt(dst, crypted[:bs])
crypted = crypted[bs:]
dst = dst[bs:]
}
return out, nil
}
Normally a key will be 24 bytes for DESede/3DES algorithm as it's basically a DES doing three times. There are three steps for encrypting the data.
- Get three keys from the original key, basically divide the 24 bytes into three 8 bytes key(k1, k2, k3)
- Use k1 to encrypt the original data
- Use k2 to decrypt the encrypted data
- Use k3 to encrypt the data again
- Return the output
The decryption is a reversed process with the key.
- Get three keys from the original key, basically divide the 24 bytes into three 8 bytes key(k1, k2, k3)
- Use k3 to decrypt the original data
- Use k2 to encrypt the decrypted data
- Use k1 to decrypt the data again
- Return the output
A sample testing looks like:
func TestENcDec(t *testing.T) {
data := []byte{0, 1, 2, 3, 4, 5, 6, 7}
key := make([]byte, 24)
key[1] = 8
encryptedData, err := DESedeECBEncrypt([]byte(data), key)
if err != nil {
fmt.Printf("error on encrypting data %s, err=%+v\n", data, err)
}
decryptedData, err := DESedeECBDecrypt(encryptedData, key)
if err != nil {
fmt.Printf("error on decrypting data %s, err=%+v\n", string(encryptedData), err)
}
fmt.Printf("original data: %v\n", data)
fmt.Printf("decrypted data: %v\n", decryptedData)
assert.Equal(t, decryptedData, data)
}
After decryption, the output is
original data: [0 1 2 3 4 5 6 7]
decrypted data: [0 1 2 3 4 5 6 7]
If padding is needed like PKCS#5 padding or Zero Padding, you can also easily implement them. Below are functions for get PKCS#5 padding data.
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
func PKCS5Unpadding(origData []byte) []byte {
length := len(origData)
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}
func ZeroPadding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padText := bytes.Repeat([]byte{0}, padding)
return append(ciphertext, padText...)
}
func ZeroUnPadding(origData []byte) []byte {
return bytes.TrimFunc(origData,
func(r rune) bool {
return r == rune(0)
})
}
You may want to update the encryption/decryption to take consideration of padding with below:
import (
"bytes"
"crypto/des"
"errors"
)
// DESedeECBEncrypt ...
func DESedeECBEncrypt(origData, key []byte) ([]byte, error) {
tkey := make([]byte, 24, 24)
copy(tkey, key)
k1 := tkey[:8]
k2 := tkey[8:16]
k3 := tkey[16:]
block, err := des.NewCipher(k1)
if err != nil {
return nil, err
}
bs := block.BlockSize()
origData = zeroPadding(origData, bs)
buf1, err := encrypt(origData, k1)
if err != nil {
return nil, err
}
buf2, err := decrypt(buf1, k2)
if err != nil {
return nil, err
}
out, err := encrypt(buf2, k3)
if err != nil {
return nil, err
}
return out, nil
}
// DESedeECBDecrypt ...
func DESedeECBDecrypt(crypted, key []byte) ([]byte, error) {
tkey := make([]byte, 24, 24)
copy(tkey, key)
k1 := tkey[:8]
k2 := tkey[8:16]
k3 := tkey[16:]
buf1, err := decrypt(crypted, k3)
if err != nil {
return nil, err
}
buf2, err := encrypt(buf1, k2)
if err != nil {
return nil, err
}
out, err := decrypt(buf2, k1)
if err != nil {
return nil, err
}
out = zeroUnPadding(out)
return out, nil
}
func encrypt(origData, key []byte) ([]byte, error) {
if len(origData) < 1 || len(key) < 1 {
return nil, errors.New("wrong data or key")
}
block, err := des.NewCipher(key)
if err != nil {
return nil, err
}
bs := block.BlockSize()
if len(origData)%bs != 0 {
return nil, errors.New("wrong padding")
}
out := make([]byte, len(origData))
dst := out
for len(origData) > 0 {
block.Encrypt(dst, origData[:bs])
origData = origData[bs:]
dst = dst[bs:]
}
return out, nil
}
func decrypt(crypted, key []byte) ([]byte, error) {
if len(crypted) < 1 || len(key) < 1 {
return nil, errors.New("wrong data or key")
}
block, err := des.NewCipher(key)
if err != nil {
return nil, err
}
out := make([]byte, len(crypted))
dst := out
bs := block.BlockSize()
if len(crypted)%bs != 0 {
return nil, errors.New("wrong crypted size")
}
for len(crypted) > 0 {
block.Decrypt(dst, crypted[:bs])
crypted = crypted[bs:]
dst = dst[bs:]
}
return out, nil
}
func zeroPadding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padText := bytes.Repeat([]byte{0}, padding)
return append(ciphertext, padText...)
}
func zeroUnPadding(origData []byte) []byte {
return bytes.TrimFunc(origData,
func(r rune) bool {
return r == rune(0)
})
}
Hope this helps those who needs to use ECB mode for DESede.
Reference: https://segmentfault.com/a/1190000006183694