Giving Cybersecurity a Tweak: XTS

--

I worry about cybersecurity sometimes. My worry is that there can be knowledge gaps that can be likened to a bridge building not knowing about the bricks that will be used in building a bridge. I suppose it’s the engineering approach to cybersecurity, and it is knowing how all the component parts and methods are used to build a secure system.

The problem with disks

Before we start, it is important to know that AES encryption deals with 128-bit (16-byte) encryption blocks, whereas disks typically deal with 512-byte segments. We must thus fit 32 AES blocks into our disk segments (Figure 1). Sometimes we will have empty blocks, so we must fill these with “ciphertext stealing” blocks.

With non-full disk encryption, we typically create an AES key and encrypt the file. The encryption key is typically generated from a passphrase, and uses a key derivation function (KDF). We can also create a key pair (a public key and a private key), and then encrypt the key with our public key, and then decrypt it with our private key. With this we are thus encrypting at a file level, and which will be fairly inefficient, as we need to read the whole file in at a time. With full disk encryption, we typically read from sectors on the disk, and in a random manner.

The smallest addressable element of a disk is a block — and which is typically around 512 bytes — and these are arranged into sectors:

Disk /dev/sda: 640.1 GB, 640135028736 bytes
255 heads, 63 sectors/track, 77825 cylinders, total 1250263728 sectors
Units = sectors of 1 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x00064a1a
Device Boot Start End Blocks Id System
/dev/sda1 63 2040254 1020096 83 Linux
/dev/sda2 2040255 22523129 10241437+ 82 Linux swap / Solaris
/dev/sda4 22523191 1250258624 613867717 5 Extended

Two of the most common modes of block encryption are Electronic Code Book (EBC) and Cipher Block Chaining (CBC). With ECB we basically take blocks of 128 bits, and then cipher these:

Unfortunately the same input in the blocks will lead to the same cipher output, and leaves us open to a range of cipher attacks. With CBC the blocks are chained together, and where the output of one block feeds into another one. The IV makes sure that for the same input plaintext, we will have a different ciphertext. We initially take an IV and then EX-OR it with the first plaintext block, and then encrypt this. This block is then EX-OR’ed with the next plain text block, and so it continues:

XTS (XEX-based tweaked-codebook mode with ciphertext stealing)

Within a disk system, this is a problem, as we need to be able to randomly access sectors, without having to read all the other sectors. XTS (XEX-based tweaked-codebook mode with ciphertext stealing) is a fairly common method which overcomes these problems. For this it uses a “tweak”, and where we take an encryption key, and encrypt the sector number, and use this to X-OR the data within the sector.

We first define two keys (K1 and K2). If we use 128-bit AES, then we generate two keys which are 128 bits long. Next we define an input block (P) and a sector number (i) and a block number (j), and create a tweak (X ):

Note, ⊗ is a multiplication (within a finite field of 127). The ciphertext © for each block is then:

With ⊕, we have an EX-OR operation. To decrypt, we do the opposite:

The plaintext (P) for each block is then:

In this way, we just need the encryption key, and the sector number, in order to encrypt and decrypt. The tweak acts as an IV. This process is illustrated by Figure 2.

Figure 2: Reference: [here]

Overall a sector of a size of 512 bytes will store 32, 16-byte AES-blocks (a block in AES is 128 bits long). In this way, for a single sector, we call the encrypt/decrypt method 32 times using the same i value, but different j values (0 to 31).

IEEE Standard 1619

Most fully encrypted disks now use the AES-XTS block cipher mode, and which is defined in IEEE Standard 1619. It is now implemented in many USB encrypted devices, and with Microsoft Bitlocker for Windows 10, TrueCrypt, VeraCrypt, OpenSSL, and Mac OSX FileVault.

The following is some sample Go code [here]:

package mainimport (
"golang.org/x/crypto/xts"
"crypto/aes"
"encoding/hex"
"crypto/sha256"
"bytes"
"fmt"
"os"
"strconv"
)func PKCS5Padding(ciphertext []byte, bSize int) []byte {
padding := bSize - len(ciphertext)%bSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}func PKCS5Trimming(encrypt []byte) []byte {
padding := encrypt[len(encrypt)-1]
return encrypt[:len(encrypt)-int(padding)]
}func getSHA1(s string) []byte { hasher := sha256.New()
hasher.Write([]byte(s))
key:=hasher.Sum(nil)
return key
}
func main() {
var sector uint64 password:="test"
message:="hello"
sector=100
argCount := len(os.Args[1:]) if (argCount>0) {message = string(os.Args[1])}
if (argCount>1) {password = string(os.Args[2])}
if (argCount>2) {sector,_ = strconv.ParseUint(os.Args[3],10,64)} key:=getSHA1(password)
c, _ := xts.NewCipher(aes.NewCipher,key )
fmt.Printf("Message:\t%s\nPassword:\t%s\nSector:\t\t%d\n\n\nKey (hex):\t%s\n",message,password,sector,hex.EncodeToString(key))
plaintext := PKCS5Padding([]byte(message),16)
ciphertext := make([]byte, len(plaintext))
c.Encrypt(ciphertext, plaintext, sector) fmt.Printf("Cipher:\t\t%s\n",hex.EncodeToString(ciphertext)) decrypted := make([]byte, len(ciphertext))
c.Decrypt(decrypted, ciphertext, sector)
fmt.Printf("Decrypted:\t%s",string(PKCS5Trimming(decrypted)))
}

A sample run is [here]:

Message:    Hello
Password: qwerty
Sector: 202
Key (hex): 65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5
Cipher: 99beddc752083a8b190f3fdfb8b09872
Decrypted: Hello

And, here’s a Python program [here]:

import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
import sys

message="Hello"

keysize=32
iv = os.urandom(16)

if (len(sys.argv)>1):
message=str(sys.argv[1])

keysize=32

key = os.urandom(keysize)

padder = padding.PKCS7(128).padder()

unpadder = padding.PKCS7(128).unpadder()


try:

cipher = Cipher(algorithms.AES(key), modes.XTS(iv))
encryptor = cipher.encryptor()



str=padder.update(message.encode())+padder.finalize()



ciphertext = encryptor.update(str ) + encryptor.finalize()

# Now decrypt

decryptor = cipher.decryptor()

rtn=unpadder.update(decryptor.update(ciphertext) + decryptor.finalize())+unpadder.finalize()



print("Type:\t\t\t",cipher.algorithm.name)
print("Mode:\t\t\t",cipher.mode.name)
print("Message:\t\t",message)
print("Message with padding:\t",str)
print("\nKey:\t\t\t",key.hex())
print("IV:\t\t\t",iv.hex())
print("\nCipher:\t\t\t",ciphertext.hex())
print("Decrypt:\t\t",rtn.decode())
except Exception as msg:
print(msg)

And a sample run [here]:

Type:    AES
Mode: XTS
Message: Hello
Message with padding: b'Hello\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b'

Key: 579387fc00df2e688849d603277a5b506921e6291d1b3baf05d40fbb015c795a
IV: ce0b1ab6fb4143f69c008e8ae363610e

Cipher: 87acdefc276b17be4050a69590dcfa49
Decrypt: Hello

The Tweak keys and BitLocker

Within BitLocker, the Tweak keys are protected by the Volume Master Key (VMK):

XTS is not perfect, but it is the de-facto full disk encryption mode. You should always try and encrypt your files at their source, and not rely on full disk encryption

Conclusions

Oh, go, learn something deeply.

--

--

Prof Bill Buchanan OBE FRSE
ASecuritySite: When Bob Met Alice

Professor of Cryptography. Serial innovator. Believer in fairness, justice & freedom. Based in Edinburgh. Old World Breaker. New World Creator. Building trust.