Splitting cryptographic key with openssl and bash

I have this idea, where I want to split a symmetric key into multiple parts then put those parts in multiple places so if some part is leaked, the whole system is not immediately compromised.
In my scenario, you are DevOps engineer and you use multiple tools to provision your application using GitOps paradigm. One of the tools is a secret storage, where you have your deployment secrets, like the API keys. You do not completely trust such storage as it can be a single point of compromise. You would like to encrypt the secret before you store it in the secret storage, then decrypt and use it during the pipeline run.

The tools you have available for the job:

Step 1 - Encrypt the secret

As a first step, let’s encrypt the secret and store it to the secret storage. Let’s agree that the problem of access management to the secret storage during the pipeline run is out of scope.

Generate random symmetric key

$ key=$(openssl rand -hex 16)
$ echo $key
79369741ac78beaf00a4c7f45964acac

Encrypt our secret (hello world) by using AES ECB and encode the output with base64. (Yes, the ECB is wrong, but let’s agree we would encrypt just the single block and always with the random key…)

$ echo "hello world" | openssl enc -a -aes-128-ecb -K $key
7wQz7bOPenNXDAzDmV8Qag==

Let’s test decryption

$ echo "7wQz7bOPenNXDAzDmV8Qag==" | openssl enc -d -a -aes-128-ecb -K $key
hello world

Now we store 7wQz7bOPenNXDAzDmV8Qag== to the secret storage.

Step 2 - Split the key

Neither of the tools we use - git, pipeline runner, the registry or the secret storage, is fully trusted. Each of them can be compromised in isolation. There is no ultimate solution on what to do in such scenario, but there is a way how to minimize our exposure in case that not everything is completely compromised at the same time.

What if we can set up multiple keys, so that, if there is a single system compromised, we would not lose our secret? Let’s split the AES key into 3 parts, where we can hardcode one key part to each system. Then we can reconstruct the key and decrypt the secret during the pipeline run but not have it in the clear form any time before or after the usage.

The pipeline runs in the context of our git repository so we can put one key part to files committed. The pipeline is executed in a Docker container, which is being pulled from the artifact registry. We can hardcode the second key part to the container image. Finally, as the pipeline is running in a GitLab (or any other) runner, we have a support for secrets. Those are exposed as an environment varible, so we can put the third part there.

How it would look like? We will use XOR to split the generated cryptographic key. I will use dirty bash version downloaded from the internet as it’s easier to demonstrate this. Having binary that can take multiple strings as input and xor them together would be preferable.

xor.sh file we will use:

#!/bin/bash
# BASH function to get the result
# of a ^ b when a, b are in the
# following hexadecimal string
# form: AF396463D8705 ...

# Obtained from here:
# http://www.codeproject.com/Tips/470308/XOR-Hex-Strings-in-Linux-Shell-Script
# Author is Sanjay1982 (see http://www.codeproject.com/Members/Sanjay1982)

# Usage:
# $ xor AB20FF40 DD14FABC
function  xor()
{
	local res=(`echo "$1" | sed "s/../0x& /g"`)
	shift 1
	while [[ "$1" ]]; do
	    local one=(`echo "$1" | sed "s/../0x& /g"`)
	    local count1=${#res[@]}
	    if [ $count1 -lt ${#one[@]} ]
	    then
	          count1=${#one[@]}
	    fi
	    for (( i = 0; i < $count1; i++ ))
	    do
	          res[$i]=$((${one[$i]:-0} ^ ${res[$i]:-0}))
	    done
	    shift 1
	done
	printf "%02x" "${res[@]}"
}

xor "$1" "$2"

To split the key, we would create two random keys and then apply XOR to create an actual key shares.

Generate two random keys. Those are only used during the generation.

random_1=$(openssl rand -hex 16)
random_2=$(openssl rand -hex 16)

Create the first key part xor_1 = key ^ random_1

$ xor_1=$(./xor.sh $key $random_1)

Create the second key part xor_2 = xor_1 ^ random_2

$ xor_2=$(./xor.sh $xor_1 $random_2)

We now have 3 key parts which we can distribute to our systems.

$ echo $random_1
83437a281382623a6eddf2bd9429ec53
$ echo $xor_2
6837c276b5fcae83227222187a4d94c2
$ echo $random_2
92422f1f0a0672164c0b1751b700d43d

Step 3 - Use key parts to reconstruct the key

To reconstruct the key, we would need to XOR all three parts together (key = random_1 ^ xor_2 ^ random_2). Taking limitations of our implementation into consideration, we need a temporary tempkey variable.

$ tempkey=$(./xor.sh $random_1 $xor_2)
$ reconstructed_key=$(./xor.sh $tempkey $random_2)

Finally, decrypt our secret by using reconstructed key

$ echo "7wQz7bOPenNXDAzDmV8Qag==" | openssl enc -d -a -aes-128-ecb -K $reconstructed_key
hello world

Conclusion

This use case is just a primitive example of the idea how to distribute information across multiple systems in a way, that if there is a limited compromise of a single system, we should be able to rotate our secret before it gets misused. There are ways how to detect if someone holds the knowledge of the key parts, for example, by having them stored under some false name and having them registered as a canary token in a honeypot. However, if a sophisticated attacker understands the architecture and is able to monitor our pipeline runner during the key reconstruction, the plain value of the secret would be available and we are screwed.

Sources used:

https://stackoverflow.com/questions/11477364/how-do-i-separate-an-encryption-key-into-parts https://gist.github.com/nmcv/4690672/4439a501378d605fc33e881e974f5d1dac09ca94 https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_codebook_(ECB)