Module Cryptodbm

module Cryptodbm: sig .. end

Encrypted layer over the dbm library: serverless, key-value databases with symmetric encryption.

This library provides an encrypted layer on top of the Dbm and Cryptokit packages. The improvements over Dbm are:

As a quick example, the following uncrypted bindings (key => data):

        "john-doe"        => "age 36"
        "some secret"     => "The cake is a lie."
        "Motto"           => "For relaxing times, make it Suntory time"

are stored as follows in the encrypted file (with variations depending on the password, and other parameters):

 [S~j....O.Q..tk^.2] => [...F...).Hsl..tB]
 [...y;....~.:.6V.2] => [....I...JR..w.E9..G..q=...K....b]
 [..'.C...F.x.3K.y2] => [1.)9q..M...et.b.]
 [S.....5 Y....8..2] => [.D........2..u...q.......}Z.b..z.zo.}.l3l.....>.]
 [...xD;@.8..wV..P1....e}....u..`.2] => [hb..2.._B....Y?0....|.....tM....]
 [K.#i.7j..H.ZZ.^.2] => [..z....,........]

Including several subtables in the same database file avoids having to deal with multiple files to store related information, and also prevents information leak through the number and sizes of a set of database files.

This library was primarily designed to store encrypted exam files on a university server. A common layout consists in several subtables encrypted with a global password, as well as an uncrypted subtable containing (public) meta-information.

Install and compile

Install using opam: opam install cryptodbm

Compile with ocamlbuild by adding the following to your _tags file:

<**/*> : package(cryptodbm)

Performance

I have not benchmarked this library. Keep in mind that every access (reading or writing a binding) requires to encrypt the key, and encrypt the data (when writing) or decrypt the data (when reading). Don't be pessimistic, though: it seems all right for non critical applications.

Also, there is only a global key-index for the whole table, no key-index for individual subtables. As a consequence, subtable iterators actually iterate over the whole table index (selecting only the expected subtable indexes).

A few technical details

When a database file is encrypted,

See also the project homepage.

Contact: D. Le Botlan (github.lebotlan@dfgh.met where you replace .met by .net.)



Typical example

     let table = open_append ~file:"/path/to/myfile" ~passwd:"my-secret-passwd" in

     let subtable = append_subtable table ~name:"here the subtable name" () in

     add subtable ~key:"key1" ~data:"data1" () ;
     add subtable ~key:"key2" ~data:"data2" () ;

     close table ;
     ()
   

module Error: sig .. end
All errors that may occur.

Notice that all functions may raise Error(DB_Error) when accessing the underlying database.

Basics


type 'a table 
The type of encrypted-dbm file descriptors. 'a is a phantom type precising the permission: read-only or full access.
type read 
Phantom type which represents read-only permission.
type full 
Phantom type which represents read-write permission.
type 'a subtable 
Type of a subtable. 'a is the permission.

Open, close, and flush files



The database can be opened in three modes: read mode, write (create) mode, and append mode.

Note that operations are not thread-safe at the library level: do not share a table or subtable handler between threads. However, multiple processes might access the same database, whenever the low-level dbm permits it (which depends on the low-level dbm library actually used). gdbm allows many readers in parallel, or only one writer and no reader.

open_read


val open_read : ?iterations:int ->
file:string ->
passwd:string -> signwd:string -> unit -> read table
Opens an encrypted-dbm file for reading.
Raises Returns A new handler to access this database.
iterations : Number of iterations used to map the password/signword. A higher value means a longer time to open the database. A default value is used if missing (around 12000). The same value must be used when reading/writing the database - otherwise the password is not recognized (Bad_password). The same value must be used when signing/checking the signature - oterwise the signature is not recognized (Bad_signature). The number of iterations is NOT saved in the dbfile.
file : The full path to the database file, but without the .pag or .dir extension (when applicable).
passwd : Use the given password to decrypt. Use the empty string "" if the file is not encrypted. In order to access only uncrypted bindings of an encrypted file, consider Cryptodbm.open_only_uncrypted instead.
signwd : Use the given signword to check the signature. If the signword is the empty string, do not check the signature.

open_append


val open_append : ?iterations:int ->
file:string ->
passwd:string ->
signwd:string ->
check_signature:bool -> unit -> full table
Opens an existing encrypted-dbm file in append mode.
Raises Returns A new handler to access this database.
iterations : Number of iterations used to map the password/signword. (See open_read)
file : The full path to the database file, without the .pag or .dir extension
passwd : Use the given password to decrypt.
signwd : Use the given signword to sign the database.
check_signature : Whether to check the existing signature before appending new data. If the table was not signed, raises No_signature.

open_create


val open_create : file:string ->
?overwrite:bool ->
?iterations:int ->
passwd:string ->
signwd:string ->
?max_extra_key:int ->
?max_extra_data:int ->
?max_extra_bindings:int -> perm:int -> unit -> full table
Creates a new encrypted-dbm file.
Raises Returns A new handler to access this database.
file : The full path to the database file, without the .pag or .dir extension
overwrite : Indicates if overwriting an existing file is allowed.
iterations : Number of iterations used to map the password/signword. (See open_read)
passwd : If a password is provided, use it to encrypt. An empty password means no encryption.
signwd : If a signword is provided, use it to sign. An empty signword means no signature.
max_extra_key : Max length of random padding added to keys, default 0.
max_extra_data : Max length of random padding added to data, default 0.
max_extra_bindings : (default 0): because the number of bindings of the table can be guessed without knowing the password, random extra bindings can be added to obfuscate the table.
perm : Unix permission to be used to create the file. Must allow the user to write on this file. Beware that in OCaml, 644 is not equal to 0o644.

open_only_uncrypted


val open_only_uncrypted : ?iterations:int ->
file:string -> signwd:string -> unit -> read table
Opens a table to access only uncrypted subtables.
Raises
iterations : Used to check the signature (useless if the signwd is empty).
file : The full path to the database file, without the .pag or .dir extension
signwd : Use the given signword to check the signature. If the signword is the empty string, do not check the signature.

close, flush


val close : 'a table -> unit
If in write mode, sign if necessary, add extra bindings if required, then flush and close the file. In read mode, just close the file. All the subtables are automatically closed.
Raises Error(Is_Closed) the database is already closed.
val flush : ?backup:bool -> ?backup_name:string -> full table -> unit
Sign if necessary, and flush. Optionally make a backup, that is, a copy of the current database file is made (default name is: 'filename'-backup-'date').
Raises
val get_rootfile : 'a table -> string
Returns the root filename (without the .pag or .dir extension).

Open and create subtables



create_subtable


val create_subtable : full table ->
name:string ->
?iterations:int ->
passwd:string ->
signwd:string ->
?max_extra_key:int ->
?max_extra_data:int -> unit -> full subtable
Creates a standard subtable for writing.
Raises
name : the subtable name. It can be any string.
iterations : Number of iterations used to map the password/signword. (See open_read). If unspecified, take a default value (not necessarily the value used to open the table itself).
passwd : If the passwd is empty, use the global table password. If the global table password is also empty, this subtable will be uncrypted. If the passwd is not empty, use it to encrypt this subtable. Consider Cryptodbm.create_uncrypted_subtable to create an uncrypted subtable in an encrypted table.
signwd : If a signwd is provided, use it to sign this subtable.
max_extra_key : Use this max_extra_key (instead of the global table max_extra_key parameter).
max_extra_data : Use this max_extra_data (instead of the global table max_extra_data parameter).
val create_uncrypted_subtable : ?iterations:int ->
full table ->
name:string -> signwd:string -> unit -> full subtable
Creates an uncrypted subtable (even when the global table is encrypted).
Raises
iterations : Used to check the signature (useless if the signwd is empty).
name : the subtable name. It can be virtually any string.
signwd : If a signwd is provided, use it to sign this subtable.

open_subtable


val open_subtable : 'a table ->
name:string ->
?iterations:int ->
passwd:string -> signwd:string -> unit -> read subtable
Open a standard subtable for reading.
Raises
name : the subtable name.
iterations : Number of iterations used to map the password/signword. (See open_read)
passwd : If a non-empty password is provided, use it to decrypt.
signwd : If a non-empty signword is provided, use it to check the signature of this subtable.
val open_uncrypted_subtable : ?iterations:int ->
'a table ->
name:string -> signwd:string -> unit -> read subtable
Open an uncrypted subtable for reading.
Raises
iterations : Used to check the signature (useless if the signwd is empty).
name : the subtable name.
signwd : If a non-empty signword is provided, use it to check the signature of this subtable.

append_subtable


val append_subtable : full table ->
name:string ->
?iterations:int ->
passwd:string ->
signwd:string ->
check_signature:bool -> unit -> full subtable
Open a standard subtable for appending bindings.
Raises
name : the subtable name.
iterations : Number of iterations used to map the password/signword. (See open_read)
passwd : If a non-empty password is provided, use it to decrypt and encrypt.
signwd : Use the given signword to sign this subtable.
check_signature : Whether to check the existing signature before appending new data. If the subtable was not signed, this parameter has no effect.
val append_uncrypted_subtable : ?iterations:int ->
full table ->
name:string ->
signwd:string ->
check_signature:bool -> unit -> full subtable
Open an uncrypted subtable for appending bindings.
Raises
iterations : Used to check the signature (useless if the signwd is empty).
name : the subtable name.
signwd : Use the given signword to sign this subtable.
check_signature : Whether to check the existing signature before appending new data. If the subtable was not signed, this parameter has no effect.

close


val close_subtable : 'a subtable -> unit
If in write mode, sign if necessary, then flush and close the subtable. In read mode, just close the subtable.
Raises Error(Is_Closed) the subtable is already closed.

Getters and iterators over subtables


val get_number : 'a subtable -> int
Returns this subtable's identifier (a number).
val get_name : 'a subtable -> string
Returns this subtable's name.
val iter_subtables : 'a table -> (string -> int -> unit) -> unit
Iterate over standard subtables. The function is applied to the subtable name and number.
Raises Error(Is_Closed) the table is already closed.
val iter_uncrypted_subtables : 'a table -> (string -> int -> unit) -> unit
Iterate over uncrypted subtables.
Raises Error(Is_Closed) the table is already closed.

Operations on bindings: add, find, delete, iterate


val add : ?may_overwrite:bool ->
full subtable -> key:string -> data:string -> unit
add subt key data binds key to data in subtable subt. By default, overwriting an existing binding is forbidden.
Raises
may_overwrite : if true, replacing an old binding by a new binding is permitted.
val find : 'a subtable -> string -> string
find subt key returns the data associated to key in subtable subt.
Raises
val delete : full subtable -> string -> unit
delete subt key removes the binding associated to key in subtable subt.
Raises
val clear : full subtable -> unit
clear subt removes all the bindings in subtable subt.
Raises Error(Is_Closed) the subtable is already closed.
val iter : 'a subtable -> (string -> string -> unit) -> unit
Iterate over all pairs (key, data) of the given subtable.
Raises Error(Is_Closed) the subtable is already closed.
val iterkey : 'a subtable -> (string -> unit) -> unit
Iterate over all keys of the given subtable. Faster than iter since data is not decrypted.
Raises Error(Is_Closed) the subtable is already closed.
val fold : 'a subtable -> 'b -> (string -> string -> 'b -> 'b) -> 'b
See iter.
val iter_extra_bindings : 'a table -> (string -> string -> unit) -> unit
For debugging.

Durable backup



The underlying database file is managed by the dbm library actually installed on the runtime platform. For portability, or to ensure that your data does not depend on a particular version of the dbm library, you can use these functions which convert dbm files to and from an ad-hoc, very simple binary format.
val export : read table -> binfile:string -> unit
Exports a dbfile to a durable binary format. The dbfile must be opened for reading (open_only_uncrypted suffices, in case the passwords are not known). Note that the binary file just mirrors the dbfile (that is, they both contain the same encrypted & uncrypted data).
val import : binfile:string -> dbfile:string -> unit
Imports a binary file to a dbfile. dbfile is the output dbfile, without .pag or .dir extension