An Oblivious RAM (ORAM) simulator is a compiler that transforms an algorithm in such a way that the resulting algorithm preserves the input-output behavior of the original algorithm but the distribution of the memory access patterns of the transformed algorithm is independent of the memory access pattern of the original algorithm.
The use of ORAMs is motivated by the fact that an adversary can obtain nontrivial information about the execution of a program and the data that the program is using just by observing the pattern in which the program accesses various memory locations during its execution. An adversary can get this information even if the data in memory is encrypted.
This definition is suited for settings like protected programs running on unprotected shared memory or clients running programs on their systems by accessing previously stored data on a remote server. The concept was formulated by Oded Goldreich and Rafail Ostrovsky in 1996.
A Turing machine (TM), a mathematical abstraction of a real computer (program), is said to be oblivious if, for any two inputs of the same length, the motions of the tape heads remain the same. Pippenger and Fischer proved that every TM with running time
T(n)
O(T(n)logT(n))
Informally, an ORAM is an algorithm at the interface of a protected CPU and the physical RAM such that it acts like a RAM to the CPU by querying the physical RAM for the CPU while hiding information about the actual memory access pattern of the CPU from the physical RAM. In other words, the distribution of memory accesses of two programs that make the same number of memory accesses to the RAM is indistinguishable from each other. This description will still make sense if the CPU is replaced by a client with a small storage and the physical RAM is replaced with a remote server with a large storage capacity, where the data of the client resides.
The following is a formal definition of ORAMs. Let
\Pi
n
x
\Pi
read(l)
write(l,v)
read(l)
l
write(l,v)
v
l
\Pi
\tilde{\Pi}(n,x)
C
c( ⋅ )
m( ⋅ )
C
n\inN
\Pi
n
\Pi0
m(n) ⋅ n
x
\Pi0(n,x)
c(n) ⋅ T
T
\Pi(n,x)
\mu
n\inN
x\in\{0,1\}*
1-\mu(n)
\Pi(n,x)=\Pi0(n,x)
\Pi1,\Pi2
n\inN
x1,x2\in\{0,1\}*
|\tilde{\Pi}1(n,x1)|=|\tilde{\Pi}2(n,x2)|
{\tilde{\Pi}1}'(n,x1)
\mu
{\tilde{\Pi}2}'(n,x2)
{\Pi1}'=C(n,\Pi1)
{\Pi2}'=C(n,\Pi2)
Note that the above definition uses the notion of statistical security. One can also have a similar definition for the notion of computational security.
ORAMs were introduced by Goldreich and Ostrovsky, where the key motivation was stated as providing software protection from an adversary who can observe a program's memory access pattern (but not the contents of the memory).
The main result in this work is that there exists an ORAM compiler that uses
O(n)
{O(log3n)}
n
O(1)
O(n)
O(logn)
Another important attribute of an ORAM construction is whether the access overhead is amortized or worst-case. Several earlier ORAM constructions have good amortized access overhead guarantees but have
\Omega(N)
O(log3n)
While most of the earlier works focus on proving security computationally, there are more recent works that use the stronger statistical notion of security.
One of the only known lower bounds on the access overhead of ORAMs is due to Goldreich et al. They show a
\Omega(log{n})
n
A trivial ORAM simulator construction, for each read or write operation, reads from and writes to every single element in the array, only performing a meaningful action for the address specified in that single operation. The trivial solution thus, scans through the entire memory for each operation. This scheme incurs a time overhead of
\Omega(n)
A simple version of a statistically secure ORAM compiler constructed by Chung and Pass is described in the following along with an overview of a proof of its correctness. The compiler on input and a program with its memory requirement, outputs an equivalent oblivious program .
If the input program uses registers, the output program will need
r+n/{\alpha}+polylog{n}
\alpha>1
O(npolylogn)
O(polylogn)
The ORAM compiler is very simple to describe. Suppose that the original program has instructions for basic mathematical and control operations in addition to two special instructions
read(l)
write(l,v)
read(l)
write(l,v)
The program stores a complete binary tree of depth
d=log(n/\alpha)
\gamma
\gamma0
\gamma1
\lceiln/\alpha\rceil
b=\lfloorr/\alpha\rfloor
At any point in time, there is an association between the blocks and the leaves in .To keep track of this association, also stores a data structure called a position map, denoted by
Pos
O(n/\alpha)
Pos(b)
Each node in contains an array with at most triples. Each triple is of the form
(b,Pos(b),v)
O(polylogn)
The program starts by initializing its memory as well as registers to . Describing the procedures, and is enough to complete the description of . The sub-routine is given below. The inputs to the sub-routine are a memory location
l\in[n]
input: a location, a value
Procedure FETCH // Search for the required block.
b\leftarrow\lfloorl/\alpha\rfloor
i\leftarrowl\mod\alpha
pos\leftarrowPos(b)
pos=\perp
pos\leftarrowR[n/\alpha]
\leftarrow0
(b,pos,x)
(b,pos,x)
\leftarrow1
Procedure PUT_BACK // Add back the updated block at the root.
pos'\leftarrowR[n/\alpha]
=1
(b,pos',x')
Procedure FLUSH // Push the blocks present in a random path as far down as possible.
*\leftarrow | |
pos | |
R |
[n/\alpha]
pos*
(b'',pos'',v'')
pos*
pos''
pos*
The task of the FETCH phase is to look for the location in the tree . Suppose is the leaf associated with the block containing location . For each node in on the path from root to, this procedure goes over all triples in and looks for the triple corresponding to the block containing . If it finds that triple in, it removes the triple from and writes back the updated state of . Otherwise, it simply writes back the whole node .
In the next phase, it updates the block containing with the new value, associates that block with a freshly sampled uniformly random leaf of the tree, and writes back the updated triple to the root of .
The last phase, which is called FLUSH, is an additional operation to release the memory cells in the root and other higher internal nodes. Specifically, the algorithm chooses a uniformly random leaf
pos*
pos*
The sub-routine is similar to . For the sub-routine, the input is just a memory location
l\in[n]
Let stand for the ORAM compiler that was described above. Given a program, let denote
C(\Pi)
\Pi(n,x)
\tilde{\Pi}(n,x)
\Pi(n,x)
n\inN
x\in\{0,1\}*
\Pi'(n,x)
\mu(n)
n\inN
x\in\{0,1\}*
1-\mu(n)
\Pi'(n,x)
\Pi(n,x)
It is easy to see that each and operation traverses root-to-leaf paths in chosen uniformly and independently at random. This fact implies that the distribution of memory access patterns of any two programs that make the same number of memory accesses are indistinguishable if they both do not overflow.
x1,x2\in\{0,1\}*
|\tilde{\Pi1}(x1,n)|=|\tilde{\Pi2}(x2,n)|
1-2\mu(n)
\tilde{\Pi1'}(x1,n)
\tilde{\Pi2'}(x2,n)
The following lemma completes the proof of correctness of the ORAM scheme.
\Pi'(n,x)
\mu(n)
During each and operation, two random root-to-leaf paths of are fully explored by . This takes
O(K ⋅ log(n/\alpha))
O(polylogn)
O(polylogn)
The total memory used up by is equal to the size of . Each triple stored in the tree has
\alpha+2
K(\alpha+2)
O(n/\alpha)
O(nK)
O(npolylogn)
O(polylogn)