SymbianOS
supports several methods for maintaining persistent data storage,
residing on both fixed internal and removable storage devices. At the
bottom resides a traditional file system model, accessed through the
file server process. Built on top of this resides an optional DBMS layer
than can operate in two modes—integrated within a process as a library,
accessing data stored within the process’s storage space, or as a
server process that can store and police access within a separate
storage location.
File Storage
Writable
storage devices used with Symbian devices are formatted with the VFAT
file system—a very simple format that is widely supported throughout the
computer industry. In fact, this near universal support is one of the
key reasons for its selection. Removable storage had to be capable of
being read by other devices, such as general-purpose computers.
Unfortunately, VFAT
does not support several features that assist with appropriate file
access controls, recording neither the creator/owner nor access
permissions. Instead, these are enforced by the file server process
through which file access operations are performed. The file server
dynamically evaluates whether a process is allowed to access a
particular file based on the file path, the file operation, and four
simple rules. Access to the \sys directory is restricted to processes
that contain the AllFiles or TCB capabilities. Write access to \resource
is restricted to processes that have TCB. Directories under \private
are restricted based on the SecureID of the requesting process. If the
directory name is the process SecureID, no capabilities are required;
otherwise, AllFiles is required. Finally, all other files have no access
restrictions. These rules are summarized in Table 1. These rules apply equally across all mounted file systems—Z:\ (ROM), C:\ (internal), and so on.
Table 1. Directory Access Restrictions
Path | Capability Required To Read | Capability Required To Write |
---|
\sys | AllFiles | TCB |
\resource | None | TCB |
\private\ [SecureID] | None (process with same SID) | None (process with same SID) |
| AllFiles (process with different SID) | AllFiles (process with different SID) |
\[Other] | None | None |
Data Caging
Any data that generally
should not be accessible on the device should be placed within the
executable’s private data storage. Note that two process instances
loaded from the same executable will share the same private directory
because it is keyed off the SecureID. The file server API provides
several methods for easily accessing the private directory for a given
executable, including CreatePrivatePath(TInt TargetDrive) and
SetSessionToPrivate(TInt TargetDrive).
File Handle Sharing
It
may be desirable to provide limited access to files stored within a
private directory. The file server provides support for sharing open
file handles between processes, similar in function to other platforms.
The sharing process will create a session with the file server and mark
the session as sharable by calling RFs::ShareProtected(). It will then
open the files to be shared; access controls are performed once at this
time. It will then call RFile::TransferToClient() on the target file.
The client will than call RFile::AdoptFromServer() on the received file
handle. The client now has access to the file in whatever mode it was
opened with. There are three separate transfer and adopt functions,
depending on the relationship between sharing processes. Files can be
shared from a server to a client, from a client to a server, and from a
parent to a spawned child.
In actuality, it is the
file server session that is shared with the target process. RFile
objects are actually subsessions associated with a specific session to
the file server and maintain an internal reference to the session. One
important consequence of this is that any open files within a file
server session are accessible to a client that a file has been shared
with. Whenever a file is to be shared with a target process, a new file
server session should be established and only the target files should be
opened. This prevents a malicious executable from viewing or modifying
other open files.
Structured Storage
In addition to private
file storage within an application’s private directory, there is also a
system-wide service for providing SQL database storage. Access to this
service is handled through the RSqlDatabase class, similar to how file
access is mediated through the RFs class. Databases can be created in
two ways: by specifying a filename and by specifying a database name and
a security policy. When a new database is created via filename, any
other application that can access that file location can read the
database. When a new database is created via a specified name and
policy, only applications that conform to the policy can access the
database (that is, it is shared in a secure fashion). In this case, the
database is stored within the SQL database private directory. The format
of the name for a shared secure database is
<drive>:[<SID>]dbname. To create a database, the SID must
match that of the current process.
Use the RSqlSecurityPolicy
class to define the policy for access to the secure shared database upon
creation. Once the policy is set, it cannot be changed for the lifetime
of the database. A separate policy can be set for the database schema,
global database read, global database write, table read, and table
write. This means you can create
a database where some tables can be read by any process, but others are
restricted based on capabilities, SecureID, or VendorID.
When you’re using the
RSqlDatabase object, the same concern about mixing data and statements
applies as would apply to any SQL database. Namely, be aware of SQL
injection when integrating untrusted input into database queries. Use
prepared statements, rather than raw string concatenation, in order to
safely make database queries. This is done by calling
RSqlStatment::Prepare with a descriptor containing the parameterized
query. Parameters are identified with a colon (:) prepended to them.
Then the appropriate Bind method is called for the type of data that is
to replace the parameter.
Encrypted Storage
SymbianOS does not provide a
standard encrypted storage feature. In addition, the cryptography APIs
must be downloaded and installed separately from the Symbian ^ 1 SDK.
This support is changing at a rapid pace and may be offered within the
base SDK in a future release. Unfortunately, such support may provide a
wholly different API from what’s described here.
These APIs allow a
developer to access the same cryptographic algorithms that are already
installed on the device to support, such as certificate validation and
secure communications protocols. They provide a wide-ranging set of
cryptographic-related functionality, including key exchange, asymmetric
and symmetric encryption, integrity through digital signatures and
HMACs, and message digests.
Data that should be kept
private regardless of where it is stored should therefore be explicitly
encrypted. This is readily apparent for files placed on removable
storage, but it is also important for files stored on internal media. A
determined attacker could disassemble the device to gain access to such
files. Performing encryption correctly and securely can be a confusing
proposition. The Symbian cryptographic API attempts to mitigate this by
presenting a nested and chained structure.
Proper encryption requires the
use of sufficiently random numbers derived from a pool of entropy.
Otherwise, an attacker who knows the approximate random seed (for
example, timestamp) could guess generated keys with relative ease.
SymbianOS provides the CSystemRandom class in order to obtain secure
random numbers. After instantiating an instance of the class, you should
provide a data descriptor of the desired length to the GenerateBytesL
method to obtain the required random values. Here’s an example:
LData AESKey128, HMACKey, IV;
TRandom rng;
AESKey128.SetLengthL(16);
HMACKey.SetLengthL(20);
IV.SetLengthL(16);
rng.RandomL(AESKey128);
rng.RandomL(HMACKey);
rng.RandomL(IV);
Once a
random key has been generated, it can be used to perform encryption on a
binary blob of data. A symmetric block encryption algorithm only
defines a block transformation. Given a key and a block, an apparently
random block is returned, although the same block and the same key will
always return the result. A complete system also includes padding, an
initialization vector, and a method of chaining. This is done to obscure
any structure between blocks from analysis. The SymbianOS cryptographic
libraries provide AES and 3DES, PKCS #7 padding, and CBC mode chaining,
as demonstrated here:
LCleanedupPtr< CBufferedEncryptor > encryptor(CBufferedEncryptor::NewL(
CModeCBCEncryptor::NewL(CAESEncryptor::NewL(AESKey128), IV),
CPaddingPKCS7::NewL(16)));
ctxt.ReserveFreeCapacityL(encryptor->MaxFinalOutputLength(ptxt.Size()));
encryptor->ProcessFinalL(ptxt, ctxt);
It is often assumed that
encrypted data will decrypt to garbage data when tampered with, but this
is not always the case. An encrypted blob should be integrity-protected
in order to detect when tampering has occurred. Integrity protection
can be easily provided with an HMAC, which takes a new
key and a message digest algorithm. Be sure to include the
initialization vector when calculating the HMAC verifier! This can be
seen in the brief snippet shown next.
LCleanedupPtr< CMessageDigest >
hmac(CMessageDigestFactory::NewHMACL(CMessageDigest::ESHA1, HMACKey));
LData verifier;
hmac->Update(IV);
hmac->Update(ctxt);
verifier = hmac->Final();
In order to recover
encrypted data or validate a verifier, the associated key needs to be
maintained. The data-caging mechanism previously described generally
provides sufficient protection, as long as keys are stored solely on
internal storage. For more sensitive requirements, consider
encapsulating keys by encrypting them with another key derived from a
user-supplied password. In this fashion, sensitive data can be stored
with a strong random key, and the encryption key (a much smaller piece
of data) can be protected with a password-derived key. Be sure to
generate an HMAC as well. This is demonstrated in the brief code snippet
shown next:
TPBPassword password(_L("password"));
LData derivedKey, derivedKeySalt;
TRandom rng;
derivedKey.SetLengthL(16);
derivedKeySalt.SetLengthL(8);
rng.RandomL(DerivedKeySalt);
TPKCS5KDF::DeriveKeyL(derivedKey, password.Password(),
derivedKeySalt, KDefaultIterations);
The cryptographic
libraries also include support for asymmetric operations—encrypting,
decrypting, signing, and verification. The RSA implementation can be
used for all four operations, whereas the included DSA implementation
can only be used for signing and verification. Performing each of these
actions can be seen in the following code:
LCleanedupPtr< CRSAKeyPair > kp(CRSAKeyPair::NewL(2048));
LCleanedupPtr< CRSASignature > signature;
LCleanedupPtr< CRSAPKCS1v15Encryptor > RSAencryptor(
CRSAPKCS1v15Encryptor::NewL(kp->PublicKey()));
RSAencryptor->EncryptL(pKey, cKey);
LCleanedupPtr< CRSAPKCS1v15Decryptor > RSAdecryptor(
CRSAPKCS1v15Decryptor::NewL(kp->PrivateKey()));
RSAdecryptor->DecryptL(cKey, pKey);
LCleanedupPtr< CRSAPKCS1v15Signer > RSAsigner(
CRSAPKCS1v15Signer::NewL(kp->PrivateKey()));
signature = RSAsigner->SignL(sha256hash);
LCleanedupPtr< CRSAPKCS1v15Verifier > RSAverifier(
CRSAPKCS1v15Verifier::NewL(kp->PublicKey()));
RSAverifier->VerifyL(sha256hash, *signature);