<<

Masaryk University Faculty of Informatics

Disk Encryption in Redox OS

Master’s Thesis

Tomáš Ritter

Brno, Fall 2019 Masaryk University Faculty of Informatics

Disk Encryption in Redox OS

Master’s Thesis

Tomáš Ritter

Brno, Fall 2019 This is where a copy of the official signed thesis assignment and a copy ofthe Statement of an Author is located in the printed version of the document. Declaration

Hereby I declare that this paper is my original authorial work, which I have worked out on my own. All sources, references, and literature used or excerpted during elaboration of this work are properly cited and listed in complete reference to the due source.

Tomáš Ritter

Advisor: RNDr. Petr Ročkai, Ph.D.

i Acknowledgements

I would like to thank Petr for his guidance during the process of writing of this thesis. I would also like to thank my parents for supporting me throughout my studies and not giving up on me, even though it took 6 and a half years.

ii Abstract

Rust is a fairly new that is focused on mem- ory safety while still achieving high performance. Many open-source projects are using it, among those an called Redox. The goal of this thesis is to study the architecture of the Redox operat- ing system, with the focus being on the filesystem, and implement a block-layer encryption that will work alongside it. This should result in a contribution to the Redox operating system and possibly other libraries.

iii Keywords rust, redox, disk encryption ...

iv Contents

1 Introduction 1 1.1 Rust Programming Language ...... 2 1.2 Organization of the thesis ...... 5

2 Redox OS 6 2.1 Why write an operating system in Rust ...... 6 2.2 URLs, Schemes and Resources ...... 6 2.3 ...... 8 2.4 Filesystem ...... 9

3 Disk Encryption 11 3.1 Keys in encryption ...... 11 3.1.1 Salt and key derivation function ...... 11 3.1.2 Key management ...... 12 3.2 Filesystem and block device encryption ...... 14 3.2.1 Stacked filesystem encryption ...... 15 3.2.2 Block device encryption ...... 15 3.2.3 Comparison ...... 15 3.3 Stream and block ciphers ...... 16 3.4 Block cipher modes of operation ...... 17 3.4.1 Electronic Code Book - ECB ...... 17 3.4.2 Cipher Block Chaining - CBC ...... 18 3.4.3 XEX-based tweaked-codebook mode with ci- phertext stealing – XTS ...... 19 3.5 Initialization vector ...... 22

4 High-Level Design 24

5 Implementation 27 5.1 First iteration ...... 27 5.1.1 Cipher trait ...... 27 5.1.2 The typenum and generic_array crates . . . . . 28 5.1.3 Crates which implement block cipher and block cipher modes of operation ...... 29 5.1.4 Initialization vector generation ...... 31 5.1.5 Putting all the parts together ...... 31

5.2 Second iteration ...... 33 5.2.1 Volume metadata ...... 33 5.2.2 Creating the filesystem ...... 34 5.2.3 Mounting the disk ...... 36 5.3 XTS addition ...... 37 5.4 Replacement of dynamic dispatch ...... 38 5.4.1 Enumerated types in Rust ...... 38 5.4.2 Using crate enum_dispatch ...... 40 5.5 Testing ...... 41

6 Conclusions 43

Bibliography 44

Index 46

A Electronic attachment 46

List of Figures

2.1 Difference between the monolithic kernel and the microkernel, from [10] 8 2.2 High level structure of the Redox filesystem 10 3.1 Structure of a LUKS header, from [15] 13 3.2 Recovery process of the master key 14 3.3 ECB encryption, from [19] 17 3.4 ECB decryption, from [19] 18 3.5 Comparison of encryption between ECB and CBC, which results in seemingly pseudorandom output, from [19] 18 3.6 CBC encryption, from [19] 19 3.7 CBC decryption, from [19] 19 3.8 XTS encryption without ciphertext stealing, from [19] 20 3.9 XTS encryption showing ciphertext stealing, from [19] 21 3.10 Computation of the tweak value 22 4.1 Architecture of the block-layer encryption module 25 5.1 Disk trait from redoxfs without the size function 28 5.2 Cipher trait 28 5.3 Generic array within a structure using const generics 29 5.4 Generic array within a structure using GenericArray crate 29 5.5 C++ generic array within a structure 29 5.6 The BlockCipher trait used to generically implement block modes of operation in block_modes for block ciphers 30 5.7 Trait that generators of initialization vectors need to implement 31 5.8 Writing of a u128 number as big endian 31 5.9 Cipher implementation 32 5.10 Example use of static lifetime 32 5.11 BlockEncrypt structure with a dynamically dispatched Cipher implementation 33

vii 5.12 Definition of C-type packed header aligned to the sizeof the filesystem block 34 5.13 Generic serialization of a structure to a byte array 34 5.14 Rust enumerated type representing an IP address, from [6] 38 5.15 Rust enumerated type representing an IP address with enumerated type values, from [6] 39 5.16 Pattern matching on enumerated types with enum items 39 5.17 Usage of pattern matching on structures implementing Cipher trait 39 5.18 Usage of enum_dispatch 40 5.19 Example of enum_dispatch not changing the syntax of the call 41 5.20 Setup for unit testing in Rust 42

viii 1 Introduction

The demand for new technologies with ever-increasing performance, safety and usability has never been higher. Every day there seems to be some new buzzword and associated technique, program or device aiming to solve the problems within a particular domain. Often, it dies before it gains any attention. Much less often, there comes a project that the early adopters are able to push into the mainstream, usually with the help of a major sponsor. In the area of programming languages, we have seen many technologies rise and fall, only to be used in legacy systems dependent upon it. Some languages try to have a very narrow focus and are unable to capture a substantial share of the market, while others aim too wide and lose focus of their overall goal. With the rapid pace of iteration in the software world, a lot of languages aim to "be the new" C or C++ while not providing enough benefits to abandon these time-tested tools. Even when a feature from a less known language is touted as useful, these older languages can incorporate it, rendering the new one obsolete. On the other hand, the need to keep the language backward compatible also means that a lot of features cannot be easily removed from the language. This creates friction in the language, leaving users confused about what subset of features it is recommended to use. That is where Rust comes along. With its unique focus on safety, while still having high performance, it has captured a significant user base in its relatively short existence. Major companies such as Google and Microsoft are starting to use it within their products, which shows that there is demand for such a language. [1] [2] Open-source projects ranging from simple web development, game engines and software for embedded devices are being actively developed. One of the projects is Redox OS, an experimental operating system built in Rust. It is still very much in its infancy, but provides a look into what is possible to create with features provided by this language. Redox contains many "reimplementations" of popular programs under Linux but disk encryption is currently not one of them. This thesis will focus on a simple design and implementation of block-layer disk encryption working with the currently implemented filesystem

1 1. Introduction

in Redox. It will discuss the challenges stemming from the usage of this language and how to overcome them. In the end, it will present a fully functioning block-layer encryption working with the Redox filesystem.

1.1 Rust Programming Language

Rust is a multi-paradigm programming language that supports object- oriented, functional, imperative and concurrent programming style. It aims to provide an alternative to system-level programming languages, such as C and C++, while achieving a high degree of safety and performance. Since a lot of Rust’s usage is aimed at a similar market compared to C++, many comparisons in this thesis will be made to the C++ language. Work on the language started in 2006 by Mozilla Foundation em- ployee Graydon Hoare. Mozilla became interested in the project and started sponsoring it in 2009. Work shifted to the compiler and in 2011 it became self-compiled. Rust compiler uses LLVM as its backend. The first stable version was released in 2015. Stable releases are currently released every 6 weeks. New features are developed in the nightly version, and then tested in the alpha and the beta phase for six weeks each. Syntax of Rust is somewhat similar to C and C++, but it takes a lot of its patterns from the ML family of languages and the Haskell lan- guage. Almost all parts of a function body are an expression, including the control flow operators. Return expressions are often superfluous since omitting the semicolon at the end of an expression automatically creates a return value. The Ruby language operates in the same way and its closure syntax is therefore very similar. Since Rust provides a high level of abstraction and safety but still tries to compete with C in low-level domains such as embedded and operating system development, it must provide a way to interact with hardware at a lower level. By putting the code in an unsafe block, one can for example use inline assembly or another language more suited to the domain to deal with the details. This, of course, comes at a cost since memory safety is an integral part of the language and it does not allow the null value, dangling pointers or data races in safe code which

2 1. Introduction

are checked at compile time. Putting the code in unsafe blocks turns off all the checks that prevent one from corrupting the memory, making the programmer fully responsible for its correct usage. Since there are no null pointers to signal that the value is invalid, the standard library provides an option type, which can be tested for whether it contains Some or None value. Rust uses destructive move semantics, therefore accessing a value which has been moved from results in a compile time error. This goes against C++ move semantics, where a moved from value is in an unspecified, but usable state. Rust, like C++, uses resource acquisition is initialization (RAII) paradigm instead of garbage collection, thanks to which it is able to provide deterministic management of resources with very low over- head. One of the considerable advantages of Rust is its borrow checker, which guards the validity of references by counting the amount and type of references that are given out. This also protects against data races because you are only able to give out one mutable reference to the data or an unlimited amount of immutable references. Borrow checker is one of the features that is difficult to become accustomed to, but provides you with many guarantees that would otherwise be harder to achieve. [3] All these checks are done at compile time, and the borrow checker also makes sure that all the references are valid. Lifetimes along with the borrow checker make up the ownership model of Rust. Lifetimes denote the scope at which the variables and references are viable. Most of the lifetimes within a program are im- plicit, hidden behind sugar syntax. That means that a programmer encounters them mostly when dealing with more complex cases since the compiler can usually deduce the lifetimes within the program. [4] For example, when a structure contains a reference to a variable, one needs to explicitly denote the lifetime of that reference to prevent any dangling references. Rust does not support inheritance, but it gives the programmer the ability for structures to implement interfaces, which are called traits, as a mechanism for dynamic dispatch. Traits are also used to achieve static dispatch using generic programming, where types can be checked at the time of the definition. This is in contrast to C++ where template parameters are duck-typed and cannot be checked until they are instantiated with a concrete type. The C++ committee is trying to address this problem in C++20 with the addition of concepts. [5]

3 1. Introduction

Static dispatch is achieved using monomorphization, where a separate copy of the code is generated for each instantiation. This results in increased compile times and binary size, but also makes the code better optimized for each use case. For Rust to be able to compete with C++ within the area of high- performance computing, it needs adequate support of metaprogram- ming, where computation is moved from runtime to compile time. Rust supports const generics and const functions but only in its nightly compiler. Const generics allow a programmer to implement types that are generic over constant values. As an example, they can be used to have a statically sized array as an attribute of a structure with the size supplied as a generic parameter. This would allow the compiler to generate more efficient code, as is the case with static dispatch. Macros are also included in the language and declarative macros are the most common kind. They are denoted with a "!" symbol and are often used to generate boilerplate code, such as in C. Unlike C though, they are a part of the generated abstract syntax tree and not replaced during the preprocessing phase of compilation. For example, printing to standard output in the standard library is implemented through a declarative macro. When talking about the Rust module system, we talk about crates and modules. Modules can be used to hierarchically split the code into logical units and manage visibility between them. A module consists of functions, structures, traits and may contain other modules. Crates are the language equivalent of libraries or packages. Each crate has an implicit root module, under which you can then define sub-modules. That means that modules allow you to partition the code within the crate itself. Cargo is the name of the Rust’s module management tool since one "ships" their crates to others with Cargo. All these points amount to a language that its users enjoy working with, even though the learning curve is quite steep and the process of becoming acquainted with features that are not present in other mainstream languages can be quite daunting. The official Rust book is a great reference to use during development. [6] Often, errors dur- ing compilation refer to specific chapters of the book to further the understanding of why the error was raised.

4 1. Introduction 1.2 Organization of the thesis

First, the necessary concepts will be introduced. Chapters 2 and 3 will focus on the design of Redox and on the contemporary disk en- cryption solutions, respectively. After giving an overview of the prob- lem domain, chapter 4 will present the design of a block encryption component working with Redox. In chapter 5, we will discuss the implementation and the process behind it, the syntax and concepts within Rust to introduce the reader to the language and its ecosystem. In the last chapter, we will conclude the discussion with what we have achieved within our implementation, any open source contributions stemming from it and the learning outcome.

5 2 Redox OS

Redox is a general-purpose operating system and surrounding ecosys- tem written in pure Rust. [7] Its aim is to provide a fully functioning Unix-like microkernel, that is both secure and free. It is partially com- patible with POSIX, and there are efforts within the community to rewrite the libc into Rust, called relibc. Redox in its current state is still very much experimental and may not run on your computer hardware. It is therefore recommended to boot it in a virtual machine.

2.1 Why write an operating system in Rust

For operating systems, safety is often considered critical. Since operat- ing systems provide a high level of abstraction over system resources, there must be a lot of emphasis placed on security. There have been numerous bugs in Linux and different libraries caused simply bya lack of memory and type safety. Rust avoids these pitfalls by enforcing memory safety at compile time. At the time of writing of this thesis, the Redox kernel contains around 20K lines of code with about 1000 of them being within unsafe blocks. High level design can still be a major source of issues, but it is more transparent. The design of Linux and BSD is secure, but the implementation leaves a lot to be desired in terms of safety. Looking at kernel vulnerabilities in Linux, it can be seen that a lot of bugs originate from unsafe conditions such as buffer overflows, which Rust eliminates, and not the overall design. [8] The hope of the contributors to the project is therefore that their effort will result in a more secure operating system.

2.2 URLs, Schemes and Resources

The Redox operating system and its components are based on URLs. URL is an identifier of a resource. It consists of two parts:

• The scheme part that represents the receiver of the call.

6 2. Redox OS

• The reference part that represents the payload of the URL, what the URL refers to. Let us consider a file: as an example. A URL starting with file: contains only a reference that is a path to a file. The reference then can be any arbitrary byte string. Parsing, interpretation, and storage of the reference is left to the scheme, which means it does not have to be a tree-like hierarchical struc- ture.

URLs are opened to schemes, which in turn can be opened to yield a resource. They need to have unique identifiers for the operating system to properly identify them. Schemes are a generalization of a , but they do not have to represent normal files. They can be thought of as virtual files, with certain operations defined on them. Schemes are a very useful abstraction and are used as the main communication primitive. They can be defined both in and kernel space, but the principle of least privilege applies. Resource can be defined as a data type with these operations:

1. read - Read N bytes to a buffer provided as an argument. 2. write - Write a buffer to the resource. 3. seek - Seek the resource. Some resources may not support this operation. 4. close - Close the resource. If exclusive access was given to the resource, the lock should be released.

URLs, schemes, and resources are a unified model for inter-process communication. One of the main design principles in Redox is "Every- thing is a URL". It is a generalization of "Everything is a file" from Unix, which means that resources such as hard-drives, keyboards, computer mice, documents, directories, printers, and even certain network inter- faces are streams of bytes that are exposed to the system through the filesystem. Since a URL is just an identifier of a scheme and aresource descriptor, it can be used more effectively within the system without small hacks to make it work. On Linux, you end up with recursive situ- ations where the hard-disk contains a root filesystem, which contains a folder named dev with device files including sda which contains the root filesystem. Having applications communicate with each other, with daemons or with the system through the unified API of URLs provides you with a consistent and clean interface.

7 2. Redox OS

Figure 2.1: Difference between the monolithic kernel and the micro- kernel, from [10]

2.3 Microkernel

The Redox OS is based on a microkernel and is heavily inspired by the operating system. [9] The microkernel in its purest form provides no operating system services at all except for those mecha- nisms that are used to implement such services. These mechanisms are low-level address space management, inter-process communication and thread management. The philosophy behind is that any application that can run in user space should run in user space and that the kernel space should only be used by the most essential components. Traditional operating system functions that are built around monolithic kernels, such as device drivers, protocol stacks and filesystem are therefore removed from the kernel and run inuser space. The difference can be seen in Figure 2.1. There are many advantages that come with the microkernel archi- tecture:

• Modularity - They allow finer-grained control compared to monolithic kernels, in which the components are strongly bound to the kernel. That makes any modification much more risky, since you have to modify the kernel itself. Microkernels on the

8 2. Redox OS

other hand, are very modular and you can replace, remove, change modules at runtime without modifying the kernel itself. • Security - Since microkernels follow the principle of least privi- lege, where all components only have the privileges absolutely needed to provide their functionality, they are inherently more secure compared to monolithic kernels. Many security bugs in monolithic kernels stem from the fact that services are running without restrictions in kernel mode. • Stability - Compared to microkernels, monolithic kernels tend to crash more. If a driver crashes in a monolithic kernel, it can mean a crash of the whole system. On the other hand, there is a clear separation of concerns within microkernels. One of the main disadvantages is often performance. Since a lot of the system functionality is provided by user space processes, micro- kernels tend to require more context switches. The difference between monolithic kernels and microkernels has been getting smaller over time, which can be attributed to the need to optimize a smaller amount of code. Redox is still relatively slow, since there has not been a lot of time and resources invested into optimizing it.

2.4 Filesystem

The Redox filesystem, or RedoxFS, is extent-based. An extent is acon- tiguous area of storage reserved for a file in a filesystem, represented as a range of two block numbers. Allocation size of blocks is 4096 bytes. Generally, extent allocation results in less file fragmentation. The main design inspiration behind the filesystem is UFS and Ext2, a simple hierarchical structure with Unix permissions. [11] [12] There had been work done on a new type of filesystem for Redox, TFS, which has been inspired by several ideas behind ZFS while being more modular and easier to implement. [13] Howewer, work on it has supposedly been abandoned. Overview of the design of RedoxFS can be seen in Figure 2.2. Node may either represent a file, a directory or a symbolic link. This infor- mation is represented within the mode field, which also contains user permissions. Next it contains a unique id, name, block containing its parent id, id of the next block to read and an array of extents.

9 2. Redox OS

Header Node

+ signature + mode Filesystem + version + uid + uuid Use Use + disk: Disk + name + size + parent + root + next + free 1 + extents + padding

Use

Use

<> Disk DiskCache Extent

+ read_at(block, buffer) + disk: Disk + block + write_at(block, buffer) + length + size(block) 1

DiskFile

+ file: File

Use

Block Device

Figure 2.2: High level structure of the Redox filesystem

DiskCache and DiskFile implement the Disk trait, which pro- vides a simple interface for reading and writing to a block device. Filesystem and DiskCache contain a statically dispatched implemen- tation of the Cipher trait, which is DiskCache and DiskFile, respec- tively. The header is written to the block device when the filesystem is created and is then used when mounting the disk.

10 3 Disk Encryption

With all the data that is currently stored on computers and other de- vices, such as USB flash drives, we need a way of properly securing them from being accessed by malicious third parties. Encryption al- lows us to transform the data from the original format to a format that is unreadable for a third party as a way of protecting the confiden- tiality and integrity of the data. This transformation is reversible and supports the use of different encryption algorithms with one or more encryption keys. In this chapter, we will discuss the encryption solutions used to- day, e.g. dm-crypt, and the individual components within them. This chapter will only serve as a gentle introduction to the subject in or- der to gain an idea of the general principles behind disk encryption. It will not serve as a comprehensive dive into the subject and the cryptographic theory upon which it is built. [14]

3.1 Keys in encryption

Keys are a fundamental part of any encryption schema. Passwords, i.e. passphrases provided by the user are often insecure since they are usually comprised of simple consecutive numbers or characters and whole words. These passphrases are not used directly as keys because their length can vary and different ciphers have different requirements regarding the length of the key. Instead, a hash function is used to obtain a digest of desirable length. This creates a vulnerability to a dictionary attack, which can simply iterate through pre-computed digests when trying to obtain the passphrase. With today’s computing power, the passphrase can be cracked within seconds. [15] This is the reason why we need to make the key more random and harder to compute, so that the likelihood of obtaining the passphrase through brute-force approach is nearly impossible.

3.1.1 Salt and key derivation function

Salt consists of randomly generated bits that are used in combination with the passphrase to generate a key. The usage of salt makes the

11 3. Disk Encryption

resulting key more random and protects against dictionary attacks. The passphrase and salt are used as an input to the key derivation func- tion. The generated salt is stored so that it can be used in subsequent authentications. The key derivation function produces a digest from the supplied passphrase and salt in a computationally expensive way. In order to do that, multiple iterations are used to generate the final digest. These iterations must be inherently serial, so that the attacker is not able to parallelize the algorithm and compute them simultaneously. Modern key derivation functions such as Argon2 allow the user to specify the degree of parallelism, i.e. the number of threads that are used to compute the digest and also the amount of memory to be used. [16] If the disk gets inserted into a computer with less than the required amount of memory, then it will not be possible to compute the digest to decrypt it. These types of functions are also used to stretch the passphrase, since different ciphers require different sizes of keys and this kindof passphrase typically does not have the desired properties to be used directly as a key. Because of key derivation functions being computationally expen- sive, the number of iterations that can be chosen is limited by the amount of time that the users are willing to wait for. Even the waiting time of a few seconds will make any kind of brute-force attack virtually impossible.

3.1.2 Key management

State-of-the-art encryption systems, such as dm-crypt and its key man- ager LUKS, use a two-level hierarchy of keys for encryption/decryption of the disk. The main reasons are:

• Support of multiple users • Changing of the password should not result in decryption with the old key and encryption with the new one

The LUKS header is described in Figure 3.1. The partition header contains the information about the encryption algorithm, the block mode of operations, the length of the master key, and its digest. With-

12 3. Disk Encryption

out such a header, the user would always have to provide all the encryption settings when mounting the disk.

Figure 3.1: Structure of a LUKS header, from [15]

The master key is duplicated within each key slot and it is en- crypted with a different user key. This allows multiple users to access the master key without having to share one passphrase. The key used to encrypt the master key is the digest provided by the key derivation function. Anti-forensic splitting and merging is a way of preventing mali- cious parties from recovering the key on a discarded drive or securely destroying the key. In simple terms, it expands the key so that it is distributed across the block device in such a way, that each chunk is dependent upon the previous chunk and all chunks are unique. If any chunk is missing or is modified during merging, it fails and the key is not recovered. The process of recovering the master key is described in Figure 3.2. The user inputs his passphrase, while the salt and iteration count is read from the user key slot. The digest is then used as a key to decrypt the master key which needs to be merged. After merging, a candidate master key is obtained, from which a digest is again created. The digest is compared with a master key digest from the partition header. If they match, the candidate key is the master key and the volume can

13 3. Disk Encryption be mounted. If they do not match, the candidate key is not the master key and the volume is not mounted.

Salt Iteration count

User

Key derivation Passphrase function

Derived key

Split Encrypted master master key Anti-forensic key Decrypt merge

Candidate key

Candidate digest Candidate digest yes Key derivation == function Master key digest

Salt Iteration count no

Figure 3.2: Recovery process of the master key

3.2 Filesystem and block device encryption

Disk encryption methods function in such a way that the operating system sees the data in the unencrypted form as long as the container is

14 3. Disk Encryption

correctly mounted with the corresponding key. The main distinctions are between the filesystem and the block-layer encryption.

3.2.1 Stacked filesystem encryption

These encryption solutions are implemented as a layer that stacks on top of an existing filesystem, meaning that all files written to afolder with enabled encryption are encrypted on the fly before the filesystem writes them to disk. Files are stored in their encrypted form, often including their filename, and their data are replaced with random- looking data of similar length. Other than that, they still exist within the filesystem as normal files. Using a special stacked pseudo-filesystem, we can unlock the folder containing the encrypted files and mount this folder onto itself orina different location, where the files appear in a readable form.

3.2.2 Block device encryption

Unlike filesystem encryption, block-layer encryption operates below the filesystem and everything that is written to the block device is encrypted. This makes the whole block device look random. There is no way to determine what kind of filesystem or files the device contains without decrypting the whole device. Mounting of the device is again done such that the device appears unencrypted when being accessed through a special folder in the system.

3.2.3 Comparison

Both stacked filesystem encryption(SFE) and block device encryp- tion(BDE) are viable and they both come with a set of advantages and disadvantages:

• SFE encrypts files and directories on existing filesystems, while BDE encrypts the whole block device. • BDE operates below the filesystem layer and does not differen- tiate what it is encrypting. SFE adds an additional layer to the filesystem and automatically encrypts/decrypts files when they are written/read.

15 3. Disk Encryption

• BDE encrypts all metadata, while SFE may only encrypt the names of the file and the directory. • SFE can be used without pre-allocating the amount of space for the encrypted container and also to protect existing filesystems without block device access. • BDE can encrypt whole hard drives including the partition tables and the swap space. • BDE is simpler and more robust, which in effect leaves smaller space for security flaws.

3.3 Stream and block ciphers

Block ciphers encrypt fixed, n-sized blocks of data one at a time. The blocks are usually 64, 128 or 256 bit long. Therefore, a block cipher taking 128 bit long blocks encrypts a sector long 4096 bytes in 256 blocks. They often combine the blocks within a sector in such a way that the N-th block is dependent upon the previous block. This way they can provide additional security guarantees. Because of this they also require an initialization vector, which is combined with the first block. Stream ciphers, on the other hand, encrypt one byte of plaintext at a time. For encryption and decryption, a pseudorandom bit generator is used. This generator creates a stream of seemingly random bytes which are then typically XORed with the plaintext. Stream ciphers are in most cases not dependent upon the previous bytes received or encrypted. This makes them more resistant to transmission noise, since the subsequent encrypted bytes after transmission error will not be affected. If we wanted to use stream ciphers for disk encryption, we would either need to generate a key or a nonce 1 for each write to the disk and this data would also need to be stored. If the key or the nonce were reused, it would open up a possibility for a reused key attack. [17] This means that for the purposes of disk encryption block ciphers are universally used.

1. A unique value that can only be used once

16 3. Disk Encryption 3.4 Block cipher modes of operation

When encrypting blocks within a sector, we often do not use the block cipher directly on each block but instead put it into a mode of operation. This mode states how dependent subsequent blocks are upon one another. In this section, we will focus on 3 of them, namely ECB, CBC and XTS. [18] As mentioned in section 3.3, stream ciphers such as OFB or CTR cannot be used. The operations in the illustrations are the following: L • - binary XOR N • - multiplication in the Galois field GF(2128) defined by the polynomial x128 + x7 + x2 + x + 1.

3.4.1 Electronic Code Book - ECB

This is the simplest encryption mode that we can use. Each block is encrypted separately, which means that if two blocks within a sector contain the same data, their ciphertext will be identical. This results in it not being able to hide data patterns well enough to be used in real applications. Since it does not provide enough confidentiality, it is not recommended to be used in cryptographic protocols. The encryption and decryption procedure is illustrated in Figure 3.3 and Figure 3.4 respectively.

Figure 3.3: ECB encryption, from [19]

Secure block encryption depends not only on the kind of cipher used, but also on its block mode of operation. The best cipher may be insecure if used in conjunction with an unsafe block mode of operation,

17 3. Disk Encryption

Figure 3.4: ECB decryption, from [19]

(a) Original (b) ECB (c) CBC

Figure 3.5: Comparison of encryption between ECB and CBC, which results in seemingly pseudorandom output, from [19]

such as ECB. We can see the how the original image Figure 3.5a gets encrypted using ECB at Figure 3.5b and using a more advanced block mode of operation, such as CBC at Figure 3.5c. The image encrypted with ECB shows that the same block input results in the same block output. Using CBC, on the other hand, results in a an image that is indistinguishable from random noise.

3.4.2 Cipher Block Chaining - CBC

In the CBC block mode of operation, each block of plaintext is XORed with the ciphertext of the previous block. The first block is XORed with the initialization vector. This means that all blocks are dependent upon all the previous blocks. The encryption process is described in Figure 3.6 and the decryption process in Figure 3.7.

18 3. Disk Encryption

Figure 3.6: CBC encryption, from [19]

Figure 3.7: CBC decryption, from [19]

The main problem of CBC is that the encryption cannot be paral- lelized. The issue does not occur when decrypting, since the ciphertext of a previous block is known. That means that a plaintext of N-th en- cryption block can be recovered using the previous block. As a conse- quence, a one-bit change to the ciphertext causes complete corruption in the corresponding encryption block of plaintext and inverts the corresponding bit in the following ciphertext.

3.4.3 XEX-based tweaked-codebook mode with ciphertext stealing – XTS

To mitigate the problems caused by blocks of CBC being dependent upon each other, we need to remove this dependency and replace it with something that still maintains the functionality that two filesys- tem blocks containing the same plaintext will not be encrypted into the same ciphertext. To do this, we need to introduce a tweak. Tweak is

19 3. Disk Encryption

created by encrypting the filesystem block number with an encryption key, after which it is XORed with the data within the filesystem block. For each encryption block after the first one, it is modified in ade- fined natural sequence in order to prevent the problem of 2 encryption blocks having the same ciphertext. XTS is based on the Xor-encrypt-xor(XEX) block mode of oper- ations. [18] XEX was designed to provide efficient encryption and decryption of the filesystem block. This is achieved by the encryp- tion blocks not being dependent upon each other, but instead being dependent upon a tweak and its natural sequence. Unlike other block modes of operation, XTS may contain two keys. The first one is used for encryption of the individual blocks andthe second, optional one is used for the creation of the tweak, e.g. for AES- 256 with XTS we may need two 256 bit keys. [20] Duplicating the first key and using it for the creation of the tweak does not harm the security properties of the XTS block modes of operations. [18] XEX only contains one key and its encryption process is identical to XTS shown in Figure 3.8.

Figure 3.8: XTS encryption without ciphertext stealing, from [19]

The block modes of operations introduced so far deal with the last incomplete encryption block by padding. This kind of encryption block arises when the size of the filesystem block is not divisible by the cipher block size, e.g. AES has 16 byte block size and a filesystem block may be 30 bytes long. The simplest type of padding involves adding zeroes up to the nearest number divisible by block size of the cipher, 32 in our example. This behaviour is inefficient since it causes the plaintext and ciphertext to differ in length.

20 3. Disk Encryption

Figure 3.9: XTS encryption showing ciphertext stealing, from [19]

Ciphertext stealing solves this problem. Ciphertext stealing is a technique for encrypting the final incomplete block without any padding. The processing of all the other blocks is unchanged except for the last two. A part of the second to last encryption block of ci- phertext is stolen to pad the plaintext of the last block which is then encrypted normally. The last two encrypted blocks then consist of a partial second to last block and a full last block. The decryption process requires the decryption of the final encryption block in order to restore the ciphertext of the previous one, which can then be decrypted. XTS builds on XEX by adding ciphertext stealing. In the case when there is no incomplete block, they behave the same way. The process is illustrated in Figure 3.9 and goes as follows:

• Key split into two equal parts, denoted as k1 and k2 • Encrypt the sector number i with k2, denoted as I N j • Compute the tweak T = I α , where α is a constant 2 in the Galois field GM(2128) defined by the polynomial x128 + x7 + x2 + x + 1 and j is the block number ( L ) L • Return ENCk1 X T T The hardest part to grasp is the computation of the tweak. The repeated multiplication can be implemented as in Figure 3.10. If j is equal to zero, return I. If j is bigger than zero:

• Left shift I by one

21 3. Disk Encryption

• If the most significant byte was 1, xor 135 to the result

• Repeat for j − 1

 I j = 0 I N aj = L h i I << 1 135MSB(I) j > 0

Figure 3.10: Computation of the tweak value

This way the tweak can be precomputed and the encryption for all except the last, possibly incomplete encryption block, can run simul- taneously. The ability for encryption of XTS to run simultaneously is a considerable advantage compared to the CBC block modes of operation. Because of these reasons, XTS is currently recommended over CBC or ECB and is heavily used in disk encryption. [18]

3.5 Initialization vector

The reason for using initialization vectors(IV) is to ensure that the encryption of two sectors with the same plaintext does not result in them having the same ciphertext. That means that for the sake of security one should not use the same initialization vector for two different sectors. The size of the initialization vector is dependent upon the size of the block cipher used. Types of initialization vectors:

• null - IV consists of zeroes. • plain - Sector number represented using little endian. • plainbe - Sector number represented using big endian. • essiv - Encrypted Sector Salt Initial Vector. Sector number is encrypted by the cipher used for encryption of the block device. To compute the key, we use a hash function on a key used for encryption of the block device, i.e. a master key, and use the digest as the key to encrypt the sector number.

It is not recommended practice to use null, since filesystem blocks containing the same plaintext would result in the same ciphertext. The next two types are numbers in a natural sequence, which means they

22 3. Disk Encryption

are predictable and therefore susceptible to a watermarking attack. [15] An attacker can create a special file which can effectively zero out the initialization vector, which exposes an underlying pattern in the ciphertext, i.e. whether a file is stored there. ESSIV eliminates this problem, since the sequence looks random. XTS uses a sector-specific tweak value, which fulfills the same purpose. Using XTS with ESSIV is therefore redundant.

23 4 High-Level Design

This chapter gives a brief introduction to the individual components needed to create a block-layer encryption module. After having in- troduced the Redox operating system and contemporary encryption solutions, we can build our own encryption module working with the Redox filesystem. A very high-level architecture of it can be seenin Figure 4.1. This omits a lot of the details but instead provides a useful overview of all the components that need to be implemented. As previously described in section 2.4, any component that needs to communicate with the block device needs to implement the Disk trait. DiskCache, DiskFile from redoxfs and BlockEncrypt imple- ment it. These structures also communicate with each other using this interface, creating a call tree starting from Filesystem, going through DiskCache, BlockEncrypt, and DiskFile, which is directly responsi- ble for writing and reading from the block device. Filesystem and DiskCache can both contain any structure implementing the Disk trait. The caching mechanism is implemented using a hash map. Every time a read or write is called, the cache checks whether the current filesystem block is in memory and proceeds. The following scenarios may occur:

• read and data in cache → return content from cache • read and data not in cache → read data from disk, decrypt, write to cache, return content • write and data in cache → write data to cache, encrypt, write to disk • write and data not in cache → write data to cache, encrypt, write to disk

Before we can start using an encrypted disk, we first need to create a filesystem and generate volume metadata that allows us to readthe encryption configuration when mounting the disk. A user needs to choose a configuration when creating the encrypted volume where they will declare what kind of block cipher, block mode of operation and initialization vector they wish to use. They will then need to input their passphrase. The encryption component will generate a master key, salt for both the master key and passphrase and compute their

24 4. High-Level Design

Filesystem Header

+ encryption_algorithm 1 + block_mode + iv_generator + user_key_salt DiskCache + master_key_encrypted impl Disk + master_key_salt + master_key_digest

1

<> impl Disk Disk Use

+ read_at(block, buffer) + write_at(block, buffer) + size(block) BlockEncrypt

+ offset + cipher: Cipher

+ create_encrypted_disk(path_to_scheme, cipher_type, block_type, DiskFile 1 iv_type, password):Self + mount_encrypted_disk(path_to_scheme, password): Self 1

CipherImpl <> Cipher + key: Array + iv_generator: IVGenerator Use + encrypt(block, buffer) + decrypt(block, buffer) + create(key, iv_generator_type): Self

1 <> IVGenerator IVGeneratorImpl + get_iv(block): Array Block Device

Figure 4.1: Architecture of the block-layer encryption module

25 4. High-Level Design

digests. The generation will come from a randomness source that is cryptographically secure. The digest resulting from concatenated passphrase and salt, i.e. a user key, will be used to encrypt the master key. Argon2 key derivation function will be used to compute the digest. [16] All this data will be serialized and saved at the start of the disk. This block or blocks will be ignored by the filesystem and only used when mounting the encrypted volume. The offset member in BlockEncrypt is used to mark the number of blocks in which the volume metadata is stored. Since different crates with block ciphers and block modes ofoper- ations have different interfaces, we need to create a wrapper around them in order to have a unified interface for BlockEncrypt. Each wrapper therefore implements the Cipher trait that provides a sim- ple encrypt and decrypt member functions. Within each wrapper, there is also an implementation of IVGenerator to provide initializa- tion vectors for each filesystem block. The input of the IVGenerator will be the filesystem block number and output will be a vector of the same size as the block size of the chosen cipher. This array will have properties dependent upon the user configuration, with options such as in section 3.5. To reduce code duplication in each wrapper, we could have the IVGenerator be a part of the Cipher trait but Rust does not support trait member fields.1 Because of this we will need to implement it separately for each wrapper.

1. There have been discussions on adding this feature to the language but not many people agree it would be a net positive. It could lead to performance issues with always having to access the variable through the vtable.

26 5 Implementation

This chapter describes the iterations the code went through, the prob- lems encountered along the way, design decisions that were made, and their implications on performance and idiomatic use of Rust.

5.1 First iteration

The focus of the section will be on getting the basic building blocks im- plemented. The main goal is getting the basic architecture down with encryption and decryption on the fly. Because of that, the password and salt will be fixed and the specifics of generating and obtaining a master key will not be delved into at all. The mounting process will also be omitted, since for simplicity of the first iteration it was hard-coded into the Redox filesystem.

5.1.1 Cipher trait

Before implementing the Cipher trait, we first need to look at the Disk trait from the Redox filesystem in Figure 5.1. Each of the functions returns Result, which is a simple wrapper for error handling in Rust. The usize type is an unsigned integer type dependant upon the architecture for which the code is compiled for, e.g. 32 bits for 32 bit machines and 64 bits for 64 bit machines. For successful reading/writ- ing of a filesystem block from/to the disk its size is returned, whereas for unsuccessful reading/writing an error is returned. All functions take a mutable instance of self by reference, which means that the state of the instance can be changed and it will not be consumed by the function call. Next, we have an identification of the filesystem block we want to read/write and a reference to a buffer which can be written to or read from. When reading into a buffer, it needs to be mutable, but themu- tability is not needed when writing from the buffer. Therefore when encrypting a buffer to write to the disk, additional memory needs to be used for this constraint to be satisfied. There are two options: 1. Allocate the memory on the caller side, copy the buffer there, and call encrypt

27 5. Implementation

pub trait Disk{ fn read_at(&mut self, block: u64, buffer: &mut [u8]) -> Result; fn write_at(&mut self, block: u64, buffer: &[u8]) -> Result; }

Figure 5.1: Disk trait from redoxfs without the size function

2. Let the underlying Cipher implementation take care of any ad- ditional allocation and return the encrypted buffer in a newly allocated memory Since the first option may result in an additional copy of the buffer which may not be needed by the underlying Cipher implementation, it is more advantageous to use the second option. That leads us to the Cipher trait in Figure 5.2. A type Vec is returned, which is a dynamically sized array of unsigned 8-bit values. It is analogous to the C++ std::vector. pub trait Cipher{ fn encrypt(&self, block: u64, buffer: &[u8]) -> Vec; fn decrypt(&self, block: u64, buffer: &mut [u8]); }

Figure 5.2: Cipher trait

5.1.2 The typenum and generic_array crates

Rust in its current state does not support const generics in its stable compiler. It is only an experimental feature with bugs, which means that there is currently no standard support for evaluation of constant generic parameters at compile time. For a language that tries to com- pete with C++ and its performance, it poses a problem with regards to the use of metaprogramming. This lead to the creation of the typenum crate. The crate provides a way to convert type-level numbers into

28 5. Implementation

their runtime counterparts, all the while being evaluated at compile time. The generic_array crate builds on top of this by using unsigned integer types from typenum as a generic parameter for its size. In the future, we should be able to write code similar to Figure 5.3, but currently, without the support of const generics, we can only write a generic array like in Figure 5.4. The C++ version of the code, which supports const generics, can be seen in Figure 5.5. struct Foo{ data: [T;N] }

Figure 5.3: Generic array within a structure using const generics

struct Foo>{ data: GenericArray }

Figure 5.4: Generic array within a structure using GenericArray crate

template struct Foo { T data[N]; }

Figure 5.5: C++ generic array within a structure

5.1.3 Crates which implement block cipher and block cipher modes of operation

The block_modes crate provides a generic implementation of block modes of operation over different ciphers written in Rust. ECB, CBC, and PCBC block modes of operation are currently implemented1.

1. XTS mode was missing at the time of writing of this thesis.

29 5. Implementation

In order for the block modes to be able to use a block cipher, the block cipher needs to implement the BlockCipher trait from the crate block-cipher-trait and parts of it can be seen in Figure 5.6. KeySize, BlockSize and ParBlocks2 are called associated types and they need to be defined in each implementation of the trait. The type ArrayLength refers to the type of an array it will be used for, e.g. KeySize and BlockSize are used as a type argument for a GenericArray containing u8. These associated types are type-level unsigned integers from the crate typenum and they can be accessed through the :: operator. This results in not needing to supply the size of the key and the size of the block separately since we can directly use these associated types from the block cipher that is passed as a type argument.

pub trait BlockCipher{ type KeySize: ArrayLength; type BlockSize: ArrayLength; type ParBlocks: ArrayLength>; fn encrypt_block(&self, block: &mut GenericArray ); fn decrypt_block(&self, block: &mut GenericArray );

Figure 5.6: The BlockCipher trait used to generically implement block modes of operation in block_modes for block ciphers

Another crate that provides block ciphers and block modes of operation is openssl. It is a thin wrapper around the openssl library and allows us to use the underlying C implementation it contains. It will not be used in this thesis for two reasons. First, there were a lot of problems with making it compile under Redox and secondly, since it uses C, it also contains a lot of "unsafe" operations from Rust’s point of view.

2. Number of blocks which can be processed in parallel

30 5. Implementation

5.1.4 Initialization vector generation

The block-layer encryption supports initialization vectors such as in section 3.5. For most initialization vectors it would be sufficient to provide just a function to generate a vector from the filesystem block number. The only problem is ESSIV which uses a hashed master key to encrypt the initialization vector, which means that the hashed key needs to be saved in order to provide a unified interface with initial- ization vector generators. IVGenerator trait can be seen in Figure 5.7. ESSIV implementation can then access the hashed master key within its structure using the reference to self.

pub trait IVGenerator>{ fn getiv(&self, block: u64) -> GenericArray; }

Figure 5.7: Trait that generators of initialization vectors need to imple- ment

Implementation of them is fairly trivial. For writing the block id in the correct endianness, we can use the byteorder crate. Then we can simply write code such as in Figure 5.8.

BigEndian::write_u128(&mut buf, block as u128);

Figure 5.8: Writing of a u128 number as big endian

5.1.5 Putting all the parts together

To implement a Cipher or IVGenerator, we first need to understand PhantomData type. When defining a structure with a type parame- ter and that parameter is not used in its whole in any of the attributes, it needs to be stored. PhantomData is a zero-sized marker and it tells the compiler to act as though it contains T even though it does not. This is needed so that the compiler can make certain safety guarantees about the structure.

31 5. Implementation

The CipherImpl structure can be seen in Figure 5.9. The only thing that has not yet been introduced is the Box type. It is a pointer type for heap allocation, which is used to achieve dynamic polymorphism. It is used in a similar way as the C++ std::unique_ptr. pub struct CipherImpl{ key: GenericArray, iv_generator: Box>, cipher_type: PhantomData, cipher_impl: PhantomData }

Figure 5.9: Cipher implementation

What is more interesting, though, is the need to add a lifetime parameter to a type parameter when its associated type gets passed further up the stack like in Figure 5.10. The ’static lifetime means that this parameter lasts for the entire lifetime of the running program. If the ’static lifetime was not there, the compiler would raise an error that explains that the associated type may not live long enough. The compiler would then recommend that the ’static lifetime be added, so that the BlockSize associated type would meet its required lifetime bounds. impl Foo{ pub fn create_bar(){ Bar::::do_something(); } }

Figure 5.10: Example use of static lifetime

All the pieces needed for a working disk encryption have been introduced. As can be seen in Figure 5.11, dynamic dispatch is used to provide an easier way of creating and working with the Cipher wrappers. This of course comes at a runtime cost because it requires the use of the virtual function table for each read/write call for the block. The IVGenerator in the CipherImpl is also implemented using

32 5. Implementation

dynamic dispatch. This may not have a big impact on performance when accounting for the time it actually takes to encrypt/decrypt a filesystem block, but it can still be improved.

struct BlockEncrypt { file: File, cipher: Box }

Figure 5.11: BlockEncrypt structure with a dynamically dispatched Cipher implementation

5.2 Second iteration

Now that a basic encryption layer is working, it needs to be extended so that it may be used in a realistic setting. What that means is mounting the block device, creating an encrypted filesystem, and generating both the master key and salt. All of the configured and generated information will have to be stored, so that any subsequent mounting of the disk will be able to read this metadata and mount the disk.

5.2.1 Volume metadata

First, the structure of the volume metadata needs to be defined. These items will be needed in order to properly use an encrypted block device:

• signature → Signature of the header, an array containing the string "BlockEncrypt". • encryption algorithm → Enumerated type, e.g. AES128. • block mode of operation → Enumerated type, e.g. CBC. • initialization vector generator → Enumerated type, e.g. Plain. • user key salt → Byte array of size 32, all the bytes are used. • encrypted master key → Byte array of size 32. In case the key size of the chosen encryption is smaller than 32, such as 16 for AES128, we will only use 16 bytes and encrypt only 16 bytes. In the case of using an encryption algorithm where the key size

33 5. Implementation

is not divisible by the block size, we will need to encrypt parts of the array that are not used for storing the key. For example, AES uses 16 byte block size, and AES192 has a key size of 24 bytes. That means that we have to encrypt all 32 bytes. For this encryption, we are using the same configuration as for the block device. • digest of master key → Byte array of size 32. Same as the size of the key. • master key salt → Byte array of size 32, all the bytes are used.

It also needs to be possible to serialize and deserialize this structure. To do that, the structure can be defined to be of C-type such asin Figure 5.12, which will guarantee its memory layout. It will be packed to the size of the block, so that it can be more easily worked with. #[repr(C, align(4096))] pub struct EncryptHeader {...}

Figure 5.12: Definition of C-type packed header aligned to the sizeof the filesystem block

The EncryptHeader structure can be transmuted into a byte array and used directly, without any additional memory allocation. This type of approach comes with a drawback from having to use an unsafe operation. Example can be seen in Figure 5.13.

unsafe fn any_as_u8_slice(p: &T) -> &[u8]{ ::std::slice::from_raw_parts( (p as *const T) as *const u8, ::std::mem::size_of::())}

Figure 5.13: Generic serialization of a structure to a byte array

5.2.2 Creating the filesystem

The first thing that needs to be done is finding all the URL schemes, as mentioned in section 2.2, that can yield a disk resource. Schemes

34 5. Implementation

can be found in the ":" directory, and the URLs under them can be listed. To make it easier, we create a simple binary that parses these schemes and prints out all the available disk schemes. Once the disk on which we want to create an encrypted filesystem is known, we need to choose the encryption type, the block mode of operation, and the generator of initialization vectors. If the values are valid, the user will be prompted to input the password two times. The termion crate can be used for this, so that the inputted password is not echoed back to the terminal. Creation of the filesystem is done by redoxfs, but first, all the meta- data needs to be generated and safely stored to the disk on the first block. For the generation of the master key and salts, a cryptographi- cally secure random number generator needs to be used. Today’s CPUs have rdrand and rdseed instructions that can provide this type of true randomness. The crate rdrand exposes these instructions through an easily usable interface in Rust. To generate the random numbers, it is advised to use rdseed to seed a true random generator implemented in software. [21] For this task, the ChaCha20 pseudorandom number generator will be used. The rdseed instruction suffers from high latency and is only usedto seed a different generator since it may fail if it is called repeatedly in a loop. That is not the case in our application but it is a good idea to stick to the recommended practice. Secondly, the Argon2 key derivation function will be used to derive the digest from both the master key and the user key. For the sake of simplicity, only the default settings will be used. With this in place, these are the steps we need to take in order to create an encrypted filesystem:

1. Open the path to the block device 2. Generate the master key, master key salt, user key salt 3. Derive the digest from the user password and salt, referred to as the user key 4. Derive the digest from the master key and salt 5. Encrypt the master key with user key 6. Create the metadata header, serialize and write to disk 7. Create an instance of BlockEncrypt and create the filesystem on the device

35 5. Implementation

5.2.3 Mounting the disk

Mounting the disk is quite simple, since the filesystem daemon is already implemented in redoxfs and exposed from the module. That means that the mount call in redoxfs can be used because the Filesystem operates above the DiskCache, which operates above our block encryption module. The only difference in mounting is therefore the need to provide a user key which can be used to decrypt the master key. Due to the fact that mounting the disk requires a background process to be run, i.e. a daemon, it proved to be impossible to read the password from the prompt after running the process in the back- ground. Another option was to run it in foreground, input the pass- word, then suspend it and run it in background, but this approach also proved problematic because of bugs within Redox. That is why, for the mounting program, the password will unfortunately have to be inserted as a command line parameter3. The sequence of mounting the encrypted block device is as follows:

1. Open the path to the block device 2. Read metadata 3. If it contains specified signature, deserialize. Otherwise, return an error 4. Derive the digest from the user password and salt, referred to as the user key 5. Decrypt the master key using the user key 6. Derive the digest from the master key and salt 7. Compare this digest with the saved one. If they differ, return an error. If they are the same, master key has been correctly decrypted. 8. Create an instance of BlockEncrypt, DiskCache and Filesystem 9. Run continuously as a daemon

3. It is a limitation of the prototype implementation and it is expected that these problems will be fixed within Redox and then this will be fixed too.

36 5. Implementation 5.3 XTS addition

Since the block_modes crate does not contain XTS block mode of op- eration, we have to implement it ourselves. As presented in subsec- tion 3.4.3, XTS is based on Xor-encrypt-xor and adds ciphertext stealing if the size of the disk sector is not divisible by the block size of the cipher. Since all sectors are of size 4096 bytes, it will not pose a problem for the block-layer encryption. XTS is defined only for AES128 and AES256, but it can also be generalized for other types of ciphers. There are two problems with the XTS block mode of operation in regards to our current design. First, XTS uses two keys, one for encryption of the block, and one for encrypting the filesystem block number. This goes against our design using the GenericArray, where the size of the array is given by the type of the block cipher that is used. This leaves us with two choices, either change the generic array to a dynamic array or create a different wrapper. Secondly, XTS in the paper is only defined for an initialization vector that is equivalent to what has been defined as a plain initializa- tion vector in section 3.5. This one is then encrypted by the secondary key and used as a tweak for each of the sector blocks. This fulfills the same function as ESSIV, so in that case the usage of other types of initialization vectors would be superfluous. The third minor reason is that AES with XTS is the current stan- dard for disk encryption, and streamlining the execution as much as possible may gain a slight performance advantage. Therefore creating a separate wrapper was chosen, even though it demands a bit more code. It is possible to implement XTS within the existing block_modes crate, where there are two ways of constructing the cipher. Either we can pass the created cipher we want with an already created tweak, or we pass the keys and the plain type of initialization vector. The keys will then be split and the second half will be used as a key for the encryption of the initialization vector.

37 5. Implementation 5.4 Replacement of dynamic dispatch

Since dynamic dispatch prevents inlining of code and better opti- mization of it, alternative ways of dispatching the code need to be considered. One option would be to pass the block cipher, the block mode of operation and the initialization vector types as generic pa- rameters to BlockEncrypt. This would result in monomorphization of all the calls and some amount of performance gain. On the other hand, there are many reasons not to do this. The increased code size may by itself become unmanagable, since every combination of the parameters would need to be accounted for explicitly at the site of creation. This would also have to be written separately for the creation of the filesystem and for the mounting. The other problems are the typical ones that come with monomor- phization, and that is increased compile times and binary size. Rust compile times are often worse than C++, but that may not be a big problem. In regards to increased binary size, that should not be prob- lematic either, since only one version would be in memory at a time, and that should not lead to problems such as instruction cache misses or stalling. Even though these two things may not pose a problem, hard to maintain code could prove cumbersome in the future, therefore we should try a different approach.

5.4.1 Enumerated types in Rust

The enumerated types used in languages such as C++ can be repre- sented in Rust such as in Figure 5.14. What makes Rust enumerated types interesting though, is their ability to contain values. The kind of value it can contain can be the same for each one or it can be different such as in Figure 5.15. Notice that trailing commas are allowed. Alge- braic types in general are implemented using enums, e.g. the standard library implements the optional type Option this way, where it can either contain Some or None. enum Ip { V4, V6}

Figure 5.14: Rust enumerated type representing an IP address, from [6]

38 5. Implementation

enum Ip { V4(u8, u8, u8, u8), V6(String),}

Figure 5.15: Rust enumerated type representing an IP address with enumerated type values, from [6]

To access the value within, pattern matching can be used as shown in Figure 5.16. As can be seen in the example, this is very useful because it can return an enumerated value encapsulating the underlying cipher. The same can be done for the initialization vector generator.

match x{ Ip::V4(a, b,_, _) => foo(a, b), Ip::V6(a) => bar(a)}

Figure 5.16: Pattern matching on enumerated types with enum items

Implementation for BlockEncrypt may then look similar to Fig- ure 5.17. This solution is quite verbose because of the need to write the same encrypt/decrypt call for each match. This would lead to a lot of bloat in the code. Luckily, there is a better solution.

pub enum CipherEnum { Aes128Cbc(Aes128Cbc), Aes128Ecb(Aes128Ecb), ...}

match cipher_enum_instance{ Aes128Cbc(cipher) => cipher.encrypt(_), Aes128Ecb(cipher) => cipher.encrypt(_), ... }

Figure 5.17: Usage of pattern matching on structures implementing Cipher trait

39 5. Implementation

5.4.2 Using crate enum_dispatch

In this section, we will use the syntactic sugar that is available in the enum_dispatch crate to implement dispatching through enums. The author claims that this type of dispatch is up to 10 times faster compared to dynamic dispatch. First and foremost, the enum_dispatch crate allows linking of the definition of the enumerated values with the desired trait. Linking is achieved through the usage of attribute macros #[...], which add attributes to the specified trait or structure. This can be seen inFig- ure 5.18. By linking them together, the crate is allowed to access the methods provided by the trait and also implements the method into(), which turns the created instance into its enumerated type.

#[enum_dispatch(CipherEnum)] pub trait Cipher{ fn encrypt(&self, block: u64, buffer: &[u8]) -> Vec; fn decrypt(&self, block: u64, buffer: &mut [u8]); } #[enum_dispatch] pub enum CipherEnum { Aes128Cbc(Aes128Cbc), Aes128Ecb(Aes128Ecb), ...}

EncryptionAlgorithm::Aes128 =>{ match cipher_mode{ CipherMode::CBC => Aes128Cbc::create( master_key, iv_generator).into(), CipherMode::ECB => Aes128Ecb::create( master_key, iv_generator).into(), ...

Figure 5.18: Usage of enum_dispatch

After this transformation, the created type can be used in the same way as dynamic dispatch was used before. The resulting implementa- tion of BlockEncrypt can be seen in Figure 5.19 with an unchanged call to decrypt/encrypt. The code "under the hood" is still the same as

40 5. Implementation

in Figure 5.17 but the sugaring allows for it to be written in a more con- cise way. The same transformation has also been done on IVGenerator, but the trait had to be changed from containing the ArrayLength pa- rameter to containing the BlockCipher instead. pub struct BlockEncrypt { cipher: CipherEnum } self.cipher.decrypt(block, buffer); let vec= self.cipher.encrypt(block, buffer);

Figure 5.19: Example of enum_dispatch not changing the syntax of the call

5.5 Testing

Rust provides integrated functionality for unit testing. The typical structure of unit tests is shown in Figure 5.20. Units tests go into the module tests, which is marked with the #[cfg(test)] attribute. Each individual test is then marked with #[test] attribute. Foo resides outside of the module since it can provide common functionality for different tests. It is made available from within a module with use super::*. Testing of the expected result with the computed one can be done using the assert_eq!() macro. Two functionalities need to be tested. First, the block-layer encryp- tion needs to be tested without any filesystem on top of it. To do that we will test the creation, mounting of BlockEncrypt and reading/writing from/to the file representing the block device. The function will takea configuration where block cipher, block modes of operation andtype of initialization vector will be specified and will reside outside ofthe tests module just as foo function in Figure 5.20. That results in being able to test all possible configurations available for the block-layer encryption using only one function. Each test will then generate a ran- dom key, create and mount BlockEncrypt, write randomly generated data to a randomly chosen filesystem block and read them.

41 5. Implementation

fn foo(){ // do something assert_eq!(expected, result); } #[cfg(test)] mod tests { use super::*; #[test] fn test(){ foo()}}

Figure 5.20: Setup for unit testing in Rust

The second way to test it is with the Redox filesystem. It has a small, supposedly incomplete test since it is full of TODO comments. The test suite is not exposed outside of the crate and is not written generically which means it cannot be used directly. Therefore, the test will be copied and changed so that it can be used with BlockEncrypt.

42 6 Conclusions

The goal of the thesis was to study disk encryption in contemporary systems and implement it in Redox OS using the Rust programming language. This has been fully realized. Throughout the thesis, we have delved deeply into the Rust programming language and the program- ming style available within. We explored the main ideas behind Redox, analyzed its filesystem, and figured out how block-layer encryption fits into it. We have seen that Rust is a fully-fledged language andthat it can compete with lower-level languages. Its performance may not be as high as C, or it may not contain every feature available in C++, but it is catching up on all these fronts while having a few notable advantages in terms of safety and ease of use. Most importantly, we have seen that it is a language worth learning. The learning curve is quite steep, but the advantages it offers make up for the difficult learning phase. As of the time of writing of this thesis, a request for merging of the XTS extension into block_modes is awaiting approval and the BlockEncrypt module has been merged into Redox. This module cov- ers the basics required from block-layer encryption and is easily ex- tensible. As for future development, new ciphers and block modes of operation may be easily added if needed. If Redox implements multiple user support, this module can also be extended to support this.

43 Bibliography

1. Google Fuchsia [online]. Wikipedia [visited on 2019-12-02]. Avail- able from: https://en.wikipedia.org/wiki/Google_Fuchsia. 2. Using Rust in Windows [online]. Microsoft [visited on 2019-12- 02]. Available from: https://msrc-blog.microsoft.com/2019/ 11/07/using-rust-in-windows/. 3. SHANKER, Sid. Fear not the Rust Borrow Checker [online]. Squidarth [visited on 2019-12-02]. Available from: http : / / squidarth.com/rc/rust/2018/05/31/rust-borrowing-and- ownership.html. 4. ZAVERSHYNSKYI, Maksym. Understanding Rust Lifetimes [on- line]. Medium [visited on 2019-12-02]. Available from: https:// medium.com/nearprotocol/understanding-rust-lifetimes- e813bcd405fa. 5. C++ extensions for concepts [online]. 2017 [visited on 2019-12- 01]. Available from: http://www.open-std.org/jtc1/sc22/ wg21/docs/papers/2017/p0734r0.pdf. 6. The Rust Programming Language [online] [visited on 2019-12-01]. Available from: https://doc.rust-lang.org/book/. 7. Redox operating system [online] [visited on 2019-12-01]. Available from: https://doc.redox-os.org/book/. 8. Linux Kernel Vulnerability Statistics [online] [visited on 2019-12- 01]. Available from: https://www.cvedetails.com/product/ 47/Linux-Linux-Kernel.html?vendor_id=33. 9. Minix [online]. Minix3 [visited on 2019-12-02]. Available from: https://www.minix3.org/. 10. Microkernel [online]. Wikipedia [visited on 2019-11-22]. Avail- able from: https://en.wikipedia.org/wiki/Microkernel. 11. UFS Filesystem (Reference) [online]. Oracle [visited on 2019-12- 02]. Available from: https://docs.oracle.com/cd/E19120-01/ open.solaris/819-2723/fsfilesysappx-94408/index.html. 12. ext2 [online]. Wikipedia [visited on 2019-12-02]. Available from: https://en.wikipedia.org/wiki/Ext2.

44 BIBLIOGRAPHY

13. The Z File System (ZFS) [online]. FreeBSD [visited on 2019-12- 02]. Available from: https://www.freebsd.org/doc/handbook/ .html. 14. FRUHWIRTH, Clemens. New methods in hard disk encryption. 2005. Technical report. 15. Block-layer encryption [online]. Markus Gattol [visited on 2019-11- 22]. Available from: https://www.markus-gattol.name/ws/dm- crypt_luks.html. 16. BIRYUKOV, Alex; DINU, Daniel; KHOVRATOVICH, Dmitry. Argon2: the memory-hard function for password hashing and other applications. 2017. 17. Stream cipher attacks [online]. Wikipedia [visited on 2019-12-01]. Available from: https://en.wikipedia.org/wiki/Stream_ cipher_attacks. 18. ROGAWAY, Phillip. Evaluation of Some Blockcipher Modes of Oper- ation. 2011. Technical report. 19. Block cipher modes of operation [online]. Wikipedia [visited on 2019-11-22]. Available from: https://en.wikipedia.org/wiki/ Block_cipher_mode_of_operation. 20. DAEMEN, Joan; RIJMEN, Vincent. Rijndael/AES. In: Encyclope- dia of Cryptography and Security. Springer US, 2005, pp. 520–524. 21. The difference between rdrand and rdseed [online]. Intel [visited on 2019-11-23]. Available from: https://software.intel.com/en- us/blogs/2012/11/17/the-difference-between-rdrand-and- rdseed.

45 A Electronic attachment

Archive thesis.tar.gz contains all the necessary source files for a successful build on Linux. After unpacking the directory, we will find two directories and a README.md file. The block-ciphers direc- tory contains the git repository of the same name with the imple- mented XTS extension and tests for it. The source file is located in block-ciphers/block-modes/src/xts.rs. The other directory, block_encrypt, contains the block encryption module that was implemented for Redox. The README.md file will walk you through the building and running process both for Linux and Redox.

46