Example: Firebase Cloud Key Storage

Choosing a Cloud Key Storage System

The BBM Enterprise SDK uses cryptographic keys to protect communications. These keys are stored and distributed in a cloud storage system chosen by the application.

The BBM Enterprise SDK can use any cloud storage system that can meet some basic requirements. Firebase is an example of such a system, and this page explains how to use Firebase with the BBM Enterprise SDK.

Using Firebase with the BBM Enterprise SDK

Firebase offers a cloud-hosted NoSQL realtime database, located here: https://firebase.google.com/docs/database/.

Firebase also offers a variety of authentication system integration options that allow you to be flexible in your design. For more information, visit https://firebase.google.com/docs/auth/.

The mechanism for storing private and public security keys must adhere to the following rules:

Rules for a user's private key data:

Rules for a user's public key data:

This design also maintains an important separation between a user's regId (BBM identifier), and the identity employed by the authentication system. Therefore, you can use any drop-in authentication solution that is compatible with Firebase. For example, you can use popular federated identity providers such as Google Sign-In and Facebook Login.

An alternative design, which can authoritatively link the user's regId and their system user ID (BBM identifier), is not discussed beyond acknowledging that the data store design is made much simpler and rigorous when a custom authentication system is used to do either of the following:

  1. Set the regId (BBM identifier) as a user's OAuth UID
  2. Establish an authoritative claim in the OAuth token (which declares the user's ownership of a regId), that is guaranteed to have a 1:1 mapping with the UID by the authorization system issuing the token.

For more information on these rules, please refer to the Firebase Realtime Database documentation for Security & Rules, located here: https://firebase.google.com/docs/database/security/

Because the Firebase Realtime Database employs JSON as a means of data storage and transfer, it is recommended that all encryption, signing, and symmetric keys stored in this database be base64url encoded, without padding.

It is also recommended that any keys retrieved from the public key store be monitored for changes, to ensure that the most up-to-date copy of a user's public keys are available to the client, and are cached locally. For more information, visit:

iOS: https://firebase.google.com/docs/reference/ios/firebasedatabase/api/reference/Classes/FIRDatabaseReference#/Attaching%20observers%20to%20read%20data

Android: https://firebase.google.com/docs/reference/android/com/google/firebase/database/Query.html#addChildEventListener(com.google.firebase.database.ChildEventListener)

Security and data integrity rules

The following rules may be installed into a project's Realtime Database via the project's Firebase console, which is located here: https://console.firebase.google.com/

// All keys used in Firebase are subject to the following limitations:
// https://firebase.google.com/docs/database/web/structure-data
//
//   If you create your own keys, they must be UTF-8 encoded, can be a maximum
//   of 768 bytes, and cannot contain ., $, #, [, ], /, or ASCII control
//   characters 0-31 or 127.
//
// This will impose restrictions on the values that can be used as keys within
// the Firebase database.  The restrictions on the keys, if any, are described
// in the key description that precedes the key as it is introduced in the
// rules.
//
// The following rules may be installed into a project's Firebase Realtime
// Database via the project's Firebase console:
// https://console.firebase.google.com/
{
  "rules": {
    // The Private Key Store is used to store a user's private keys for both
    // encryption and signing purposes as well as the symmetric key for each
    // mailbox to which the user is subscribed.
    //
    // The private key store entry containing a user's regId must be the first
    // user record created.
    //
    // The private key store of a user cannot be deleted until the regId
    // ownership claimed in the regIds index has been removed.  This
    // restriction ensures that regId entries and public key store data for a
    // user cannot be orphaned.
    "privateKeyStore": {
      // Indexed by the Firebase user's OAuth ID.  The UID is system assigned
      // and requires no special treatment or encoding to be used as a key.
      "$uid": {
        // Reading is restricted to authenticated users whose UID matches the
        // UID of the record.
        ".read": "auth != null && auth.uid == $uid",

        // The following restrictions are used to determine whether a write to
        // the record is allowed:
        // 1) The user must be authenticated and their UID must match the UID
        //    of the record; and
        // 2) New data being written is always allowed (it must additionally
        //    pass verification checks); or
        // 3) Deleting a record that does not exist is always allowed; or
        // 4) The deletion of a record can only proceed if:
        //    * The claim in the regIds index associated with this record
        //      does not exist; or
        //    * The claim in the regIds index associated with this record
        //      is not associated with the UID of the record being removed.
        ".write": "
          auth != null && auth.uid == $uid &&
          (newData.exists() || !data.exists() ||
           !root.child('regIds').child(data.child('regId').val()).exists() ||
           root.child('regIds').child(data.child('regId').val()).val() != $uid)",
        // When the private key store data is being set outright, it must
        // always contain a regId member.


        ".validate": "newData.child('regId').exists()",
        // The regId is the BBM identifier for a user and it must be
        // authoritatively associated with an authorized user of the system.
        // The first part of guaranteeing a 1:1 relationship between a regId
        // and a UID is to declare that a UID 'owns' a given regId when the
        // private key store data is created.
        //
        // Because claiming ownership of a regId is a two step process, it is
        // possible to encounter races between different users who have been
        // incorrectly assigned the same regId.  A user who is unable to
        // complete the process to claim a regId must still be able to change
        // the regId associated with their UID so that they can declare their
        // intent to make a claim on another regId if they are somehow able to
        // obtain one.
        //
        // In order for the regId to be written it must meet the following
        // criteria:
        // 1) The new regId value must be a non-empty string; and
        // 2) The existing regId value must:
        //    * Not exist (new record); or
        //    * The existing regId must not yet be claimed in the regIds
        //      index; or
        //    * The existing regId must not be claimed by the current user; or
        //    * Be the same regId as the new value being claimed; and
        // 3) The new regId value must:
        //    * Not yet be claimed in the regIds index; or
        //    * Be already claimed by the current user
        //
        // The regId value is system assigned and numeric and may be inserted
        // without any additional encoding.
        "regId": {
          ".validate": "
            newData.isString() && newData.val().length > 0 &&
            (! data.exists() ||
             ! root.child('regIds').child(data.val()).exists() ||
             root.child('regIds').child(data.val()).val() != auth.uid ||
             newData.val() == data.val()) &&
            (! root.child('regIds').child(newData.val()).exists() ||
            root.child('regIds').child(newData.val()).val() == auth.uid)"
        },


        // The encryptionKey must be a non-empty string when it is being set.
        //
        // The encryptionKey value contains binary data and must be encoded as
        // a base64url string without padding.
        "encryptionKey": {
          ".validate": "newData.isString() && newData.val().length > 0"
        },


        // The signingKey must be a non-empty string when it is being set.
        //
        // The signingKey value contains binary data and must be encoded as
        // a base64url string without padding.
        "signingKey": {
          ".validate": "newData.isString() && newData.val().length > 0"
        },


        // Each mailbox symmetric key must be a non-empty string when it is
        // being set.
        "mailboxes": {
          // The mailboxId assigned by the BBM infrastructure may contain
          // characters that are illegal for use as a Firebase key.  To ensure
          // that it can be used as a key, it must be encoded as a base64url
          // string without padding.
          //
          // The mailbox key value contains binary data and must be encoded as
          // a base64url string without padding.
          "$mailboxId": {
            ".validate": "newData.isString() && newData.val().length > 0"
          }
        }
      }
    },


    // The mapping of regIds to the UIDs that have claimed them.  This is used
    // as an index to maintain data integrity and complete the 1:1 guarantee
    // between UIDs and regIds employed within this key store.
    //
    // An entry in this index authoritatively declares the UID associated with
    // the regId as the owner.  Thus, this index is also used to enforce
    // access control permissions on the public key store data (defined
    // below).
    //
    // An entry in this index must be the second user record created.
    //
    // An entry in this index cannot be deleted until the public key store
    // data associated with the regId has been removed.  This restriction
    // ensures that public key stores cannot be orphaned.
    //
    // The regId assigned by the BBM system is numeric an has no need of
    // further encoding to be used as a key.
    "regIds": {
      "$regId": {
        // No one is allowed to read/query this index.
        ".read": false,
        // The following restrictions are used to determine whether a write to
        // this record is allowed:
        // 1) A user must be authenticated to write; and
        // 2) An existing entry must be owned by the authenticated user
        //    attempting the write; and
        // 3) New data being written is always allowed (it must additionally
        //    pass verification checks); or
        // 4) Deleting a record that does not exist is always allowed; or
        // 5) The deletion of a record can only proceed if there is no
        //    public key store data associated with the regId.


        ".write": "
          auth != null &&
          (!data.exists() || data.val() == auth.uid) &&
          (newData.exists() || !data.exists() ||
           !root.child('publicKeyStore').child($regId).exists())",
        // In order for the record to written, the UID being associated with
        // the regId must meet the following criteria:
        // 1) Must be the UID of the current authenticated user; and
        // 2) The current authenticated user must have an entry in the private
        //    key store; and
        // 3) The current authenticated user's private key store entry must
        //    have a regId value declared; and
        // 4) The current authenticated user's regId value declared in their
        //    private key store entry must match the regId or the record being
        //    written.


        ".validate": "
          newData.val() == auth.uid &&
          root.child('privateKeyStore').child(auth.uid).exists() &&
          root.child('privateKeyStore').child(auth.uid).child('regId').exists() &&
          root.child('privateKeyStore').child(auth.uid).child('regId').val() == $regId"
      }
    },


    // The Public Key Store is used to store a user's public keys for
    // both encryption and signing purposes.
    //
    // To create a public key store entry, a user must first have claimed the
    // regId under which the public keys are to be advertised.  This means
    // they must first have:
    // 1) a private key store entry with the regId value
    // 2) a regIds entry associating the regId value with their UID
    //
    // Public key data may be deleted by the regId owner without restriction.
    "publicKeyStore": {
      // The public key data is stored by regId.  This allows other users to
      // find the public keys of any user for whom they have a regId.
      //
      // The regId assigned by the BBM system is numeric an has no need of
      // further encoding to be used as a key.
      "$regId": {
        // Any authenticated user can read the public key data.
        ".read": "auth != null",


        // The following restrictions are used to determine whether a write to
        // the record is allowed:
        // 1) The user must be authenticated; and
        // 2) The user must be the owner of the regId in the regIds index; or
        // 3) A record that does not exist is being deleted.
        ".write": "
          auth != null &&
          (root.child('regIds').child($regId).exists() &&
           root.child('regIds').child($regId).val() == auth.uid) ||
          (!newData.exists() && !data.exists())",


        // The encryptionKey must be a non-empty string when it is being set.
        //
        // The encryptionKey value contains binary data and must be encoded as
        // a base64url string without padding.
        "encryptionKey": {
          ".validate": "newData.isString() && newData.val().length > 0"
        },


        // The signingKey must be a non-empty string when it is being set.
        //
        // The signingKey value contains binary data and must be encoded as
        // a base64url string without padding.
        "signingKey": {
          ".validate": "newData.isString() && newData.val().length > 0"
        }
      }
    }
  }
}

Sample data

The key storage is separated into three separate sections, allowing the configuration of different access controls to the private and public data associated with each user.

{
  // The Private Key Store is used to store the private key data associated
  // with each user.  Only the logged in user whose UID matches the record may
  // read or write each the record.
  "privateKeyStore": {
    "uid1" : {
        "regId": "1234",
        "encryptionKey": "base64url encoded without padding private encryption key for user uid1",
        "signingKey": "base64url encoded without padding encoded private signing key for user uid1",

        // The mailboxId keys are also base64url encoded without padding to ensure
        // the key values are compatible with firebase key restrictions.
        "mailboxes": {
          // mailboxId1
          "bWFpbGJveDE": "base64url encoded without padding key for mailboxId1",
          // mailboxId2
          "bWFpbGJveDI": "base64url encoded without padding key for mailboxId2",
          ...
         },
     "uid2": {
        "regId": "5678",
        ...
     }
  },

  // The regId index that, in combination with the regId a UID has declared in
  // their private key store, enforces a 1:1 relationship between a regId and
  // the UID.
  "regIds": {
    "1234": "uid1",
    "5678": "uid2"
  },

  // The Public Key store is used to store the public key data associated with
  // each user.  Only the logged in user whose ownership of the regId is
  // confirmed by the regIds index may write to the record.  Any logged in
  // user may read any record in this section.
  "publicKeyStore": {
    "1234": {
      "encryptionKey": "base64url encoded without padding public encryption key for user uid1",
      "signingKey": "base64url encoded without padding public signing key for user uid1"
    },
    "5678": {
       ...
    }
  }
}