Add Encryption to Json
Intro
Sometimes you need to save sensitive information in JSON: tokens, passwords or just something that you do not want anyone else to see this e.g. your personal financial informations etc.
The post about it: how do you add encryption of part of JSON.
Use cipher & aes
There are a lot of articles with ready for copy-paste snippets about using block cyphers,
also take a look at examples in crypto/cipher
examples.
Let’s not retell them all, just give basic code examples.
Here are the basic encrypt/decrypt
functions you might write:
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"io"
)
func encrypt(key string, b []byte) (encrypted []byte, err error) {
block, err := aes.NewCipher([]byte(key))
if err != nil {
return nil, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, aesgcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
encrypted = aesgcm.Seal(nonce, nonce, b, nil)
return
}
func decrypt(key string, b []byte) (decrypted []byte, err error) {
block, err := aes.NewCipher([]byte(key))
if err != nil {
return nil, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := aesgcm.NonceSize()
nonce, encrypted := b[:nonceSize], b[nonceSize:]
if decrypted, err = aesgcm.Open(nil, []byte(nonce), []byte(encrypted), nil); err != nil {
return nil, err
}
return
}
The first one encrypts passed bytes and the second decrypts them back to plain text. There is a little nuance: due to block nature of AES you should use 16 bytes length key/password or 24 or 32 bytes (in this case will be used AES-256):
Secret Key Length | AES Variant | Block Size |
---|---|---|
16 bytes (128 bits) | AES-128 | 16 bytes (128 bits) |
24 bytes (192 bits) | AES-192 | 16 bytes (128 bits) |
32 bytes (25 6bits) | AES-256 | 16 bytes (128 bits) |
Ok, so far so good. Let’s talking about JSON. So, as you know Go has marshaling and
unmarshaling of structs and provide your a way to add custom MarshalJSON/UnmarshalJSON
for your data stypes. That’s look like the way (or place) to implemented encryption for
special data type.
Let’s create a new type e.g. EncryptedString
:
type EncryptedString string
derived (or better say - type alias) from simple, basic type string looks suitable:
everywhere we need to store sensitive information we can use that type instead of
string
, for code it will looks like the same as using string
but when you marshal this
data to JSON, it should be encrypted automatically. And when you will unmarshaling back
that encrypted data from JSON to golang struct, they should be decrypted automatically. So
that is the goal.
So for example you might write something like this:
type MoneyTransfer struct {
ID, Amount int64
CashPointAddress, ManagerBonus, Gateway string
Sender, Recipient, Description, Currency, TransferCode EncryptedString
}
here is we define something which can hold based information about money transfer: amount, currency, sender and recipient, code of transfer to pick up the money at a cash point, etc.
It also may has a lot extra, non sensitive data, which should not be encrypted for decreasing the CPU consumption and size that is why we not encrypted full JSON.
Another reason is want to has plain text with basic, non confidential data which can be used in other data pipeline processing. So in that way we can easy separate confidential data from non-confidential data.
Ok, let’s define custom MarshalJSON/UnmarshalJSON
, it is pretty easy:
func (s EncryptedString) MarshalJSON() ([]byte, error) {
b, err := Encryptor([]byte(s))
if err != nil {
return nil, err
}
return json.Marshal(b)
}
func (s *EncryptedString) UnmarshalJSON(b []byte) error {
var b1 []byte
if err := json.Unmarshal(b, &b1); err != nil {
return err
}
dec, err := Decryptor(b1)
if err != nil {
return err
}
*s = EncryptedString(dec)
return nil
}
take a look at this code, it is pretty simple (ignore the Encryptor/Decryptor
for now,
we will talk about them later): encrypt passed bytes and marshal them, unmarshal passed
bytes and decrypt them back to original strings.
Ok, here is an example how it will look:
tr1 := MoneyTransfer{
ID: 123,
Amount: 15000,
CashPointAddress: "7001 E Belleview Ave, Denver, CO 80237, USA",
ManagerBonus: "+$5.0 if processed less than 1 hour",
Gateway: "MoneyGram",
Sender: "John Doe",
Recipient: "Jane Doe",
Description: "Casio G Shock Retro 1986",
Currency: "JPY",
TransferCode: "L1K45M12XCBGAQ"
}
b, err := json.Marshal(tr1)
if err != nil {
fmt.Println(err)
}
fmt.Printf("Plain text: %#v\n\n", tr1)
fmt.Printf("Marshaled JSON: %s\n\n", b)
var tr2 MoneyTransfer
if err := json.Unmarshal(b, &tr2); err != nil {
fmt.Println(err)
}
fmt.Printf("Unmarshaled JSON: %#v\n", tr2)
this code produces output:
Plain text: main.MoneyTransfer{ID:123, Amount:15000,
CashPointAddress:"7001 E Belleview Ave, Denver, CO 80237, USA",
ManagerBonus:"+$5.0 if processed less than 1 hour",
Gateway:"MoneyGram", Sender:"John Doe", Recipient:"Jane Doe",
Description:"Casio G Shock Retro 1986", Currency:"JPY", TransferCode:"L1K45M12XCBGAQ"}
Marshaled JSON: {"ID":123,"Amount":15000,"CashPointAddress":"7001 E
Belleview Ave, Denver, CO 80237, USA","ManagerBonus":"+$5.0 if
processed less than 1 hour","Gateway":"MoneyGram",
"Sender":"YeVdx4cMLOeGOrR7skkuvalwI5oExg1Yb9S2hdMl8T1LiE1t",
"Recipient":"q/WtmxTyI5x6hvxWDKmYcj04xZ/O7PSdFzQF8OMSqVssfZrX",
"Description":"PMY16+ucUdK81JcLmhO4qDrAqrvIIRYbV5kNcORKDZhYs5Bv9Yzx4AOVKVHlD6zZsC/CcA==",
"Currency":"r5Pm1bNaHxUadIteGmCzXUaxFdAwZrwkewbXX18Cdw==",
"TransferCode":"45bCkb4cUn41/kJZcym19V8K1pie57gICtPQFc8gJA55OinTNy1xBJp7"}
Unmarshaled JSON: main.MoneyTransfer{ID:123, Amount:15000,
CashPointAddress:"7001 E Belleview Ave, Denver, CO 80237, USA",
ManagerBonus:"+$5.0 if processed less than 1 hour",
Gateway:"MoneyGram", Sender:"John Doe", Recipient:"Jane Doe",
Description:"Casio G Shock Retro 1986", Currency:"JPY", TransferCode:"L1K45M12XCBGAQ"}
as we see, the JSON string contains unencrypted data (ID, Amount, CashPointAddress, MangerBonus and Gateway) and encrypted data: Sender, Recipient, Description, Currency and TransferCode. Ok it looks good: we can safety transmit this json over network or save it in persistent storage. When we want to decrypt this data back into plain text, it is also works well - take a look at third line of output: “Unmarshaled JSON”.
So this is completely transparent way to encrypt/decrypt the part of JSON based on Go idiomatic way to marshaling/unmarshaling structs to/from JSON.
The last thing what i should clear is about encryptor/decryptor. Well, this is just a way save password somewhere is more hidden than simple variable, i thing the closure is more or less suitable for this (maybe some of you know the better way, without use 3rd party libs or services like Hashicorp Vault etc, please leave the comment!).
So, technically it needs two global vars like these ones:
var Encryptor, Decryptor func(b []byte) ([]byte, error)
we used them inside func (s *EncryptedString) UnmarshalJSON
and
func (s EncryptedString) MarshalJSON()
and two functions which initialize them:
func CreateEncryptor(key string) func(b []byte) ([]byte, error) {
return func(b []byte) ([]byte, error) {
return encrypt(key, b)
}
}
func CreateDecryptor(key string) func(b []byte) ([]byte, error) {
return func(b []byte) ([]byte, error) {
return decrypt(key, b)
}
}
These functions create closures with saved passwords inside, in that way we can simple use them without passed the password every time when we need encrypt or decrypt data.
I hope this was useful, thanks for reading!