Overview
Secure Storage Manager (SSM) securely stores data for access by authenticated apps based on the following scenarios:
- Share data with target apps - an originating application sends data to SSM, which stores the data securely to be retrieved by an authenticated target application (e.g. app configuration, security keys, etc.).
- Persist data - Persisted data remains on the device after an enterprise reset for an authenticated target app to retrieve the data securely through SSM.
This guide explains how to use SSM to share and retrieve data for consumption by a target app. In summary, the steps to share data using SSM are:
- (Optional) Pre-Encrypt data using Data Encryption Tool and send it to SSM to store.
- Configure the target app to access the data in SSM.
- Get the App Signature to authenticate the target app.
- Use Content Provider APIs in the target app to query, insert, update or delete data in SSM.
How It Works
The originating app (app supplying the data) sends data to SSM either in plain text or pre-encrypted format. The data is then internally encrypted by SSM before storing it into its database. If pre-encrypted data is received, SSM decrypts the data before performing internal encryption then storing the encrypted data into its database. The target app accesses data from SSM only after SSM ensures the authenticity of the app by validating its package name and signature (optional). When the target app retrieves the data, SSM delivers the data in the output format specified by the target app (encrypted or plain text).
Securely store and retrieve data using SSM with optional pre-encryption
SSM implements Android Content Provider to securely share data and manage data access between apps with methods to create, retrieve, update and delete (CRUD) data. Data items are structured as name/value pairs. Each pair is uniquely identified by the originating app name and data-item name, which are available to the target app. Ownership of the data, including the ability to update or delete, starts with the originating app and transitions to the target app once the data is delivered.
1. Pre-Encrypt Data (Optional)
For added data security, the originating app can optionally pre-encrypt data with Data Encryption Tool before storing data in SSM. The secret key used for the encryption must also be encrypted.
Pre-encrypt data and encrypt the secret key:
Create an AES secret key for the encryption:
private SecretKey getRandomKey(String algorithmType) {
}SecureRandom rand = new SecureRandom(); KeyGenerator generator; try { generator = KeyGenerator.getInstance(algorithmType); generator.init(128, rand); mSecretKey = generator.generateKey(); Log.d(TAG, "mSecretKey = "+ mSecretKey); // "mSecretKey" returns the secret key. return mSecretKey; } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return null;
Encrypt the data with the secret key (from step 1) using Data Encryption Tool.
Encrypt the secret key (from step 1) with the SSM public key using Data Encryption Tool. SSM public key (Base64 String):
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwE1qxpfNZVGq3wfPp3AqSeSpCPi3NUC1cCBuh5nkPvC3TfYHiozsy3gBYyUoYWIoAYlgypehqLIQfdHTrLpsVbS1BW6mnv76WvYwmaGrGfHzi50ETA8bFDwkrboG3jcHnvDJPH904BdU5eMrsq1o+BDmTmF/OAm1rJPohb8mukWh+o6OW6iNhO28IDRb26pKuTu6sckHn8I1I51bl44qaxq55A4wVR4mHEZL0EK/q2hY0Iqcak2dA8w8N0nJrWzbIbp5FeT/WyGO2pure7UxKEZfE5pkewPfcHSGp+0sbdCMaw6KrDpC5jusry4PjFw92sS/Huywv6/pv7WVPmwIDAQAB
2. Configure Target App
The target app must be configured with the proper permissions and visibility to access the data in SSM. Insert the appropriate line(s) in the client app’s manifest file (based on Java/Kotlin):
Add permission to insert / update / delete data:
<uses-permission android:name="com.zebra.securestoragemanager.securecontentprovider.PERMISSION.WRITE"/>
Add permission to query data:
<uses-permission android:name="com.zebra.securestoragemanager.securecontentprovider.PERMISSION.READ"/>
Add query provider for apps targeting API Level 30 and above:
<queries> <provider android:authorities="com.zebra.securestoragemanager.securecontentprovider"/> </queries>
Declare the authroity URI of the SSM Content Provider:
private String AUTHORITY = "content://com.zebra.securestoragemanager.securecontentprovider/data";
3. Get App Signature
For data sharing, the target app signature is required to be passed to the SSM Content Provider to authenticate the target app.
To generate the app signature:
Go to Zebra App Signature Tool. Follow instructions to download and use the tool.
Use the command below (adapt as needed) to save the target app signature (as Base64 format string) in "PackageName.txt":
Java -jar SigTools.jar GetCert -INFORM APK -OUTFORM BASE64 -IN [Client.apk] -OUTFILE [PackageName.txt]
[Client.apk] represents the app to retrieve the app signature.
Rename "PackageName" in the .TXT file name to the target app package name, e.g. "com.zebra.exampleapp.txt".
Copy the renamed "PackageName.txt" file into the /assets directory on the device for SSM to access the target app signature.
4. Use Content Provider APIs
Use Android Content Provider APIs to insert (store), update, query or delete data in SSM.
Action | Method |
---|---|
Add new data See Android reference |
Uri insert (Uri uri, ContentValues values) |
Modify/Update existing data See Android reference |
int update (Uri uri, ContentValues values, String selection, String[] selectionArgs) |
Return data based on selection criteria See Android reference |
Cursor query (Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) |
Delete data See Android reference |
int delete (Uri uri, String selection, String[] selectionArgs) |
Content Provider parameters:
Key | Value | Description | Mandatory/Optional |
---|---|---|---|
target_app_package | [string] | The target app package name and signature (in Base64 format) are stored in json format. This applies to single or multiple target apps. To generate the target app signature, see Get App Signature. | Mandatory |
data_name | [string] | Unique name to identify data. Note: Data names cannot be duplicated in the same application when inserting data. If an app tries to insert a duplicate data name, it throws a Duplicate Data Name exception. |
Mandatory |
data_value | [string or json] | Data stored for app access. | Mandatory |
data_persist_required | true false |
Set whether data can be retrieved after enterprise reset. Large data sets are not supported due to Android parcel size limitations - an exception is encountered if the query result data size is too large. | Mandatory |
data_input_form | 1 = plain text 2 = encrypted |
Set whether data received from SSM is encrypted from Data Encryption Tool. | Optional |
data_output_form | 1 = plain text 2 = encrypted |
Set whether data passed to the target app is encrypted. |
Optional |
Notes:
- When persistent data is queried after a device reboot, a delay may occur in receiving the query results until the system is completely initialized.
- Querying persistent data could be slower than querying non-persistent data, since SSM stores persistent data in a device-persistent location.
- Apps can store data only up to 10 KB in a single record. If any single data record is larger than 10 KB, it must be split and stored as multiple records with unique data names.
Insert Data
Use insert() to add and store data securely in SSM. The procedure varies depending on whether the data is being inserted for single or multiple target apps. The sample code provided demonstrates how to insert data in plain text. Rather than plain text, data can be inserted in pre-encrypted form for added security, in which case the pre-encrypted data and encrypted secret key is passed to the method; see Step 1: Pre-encrypt data.
Single App
The sample code below inserts data for access by a single target app. If successful, the return value is the URI of the newly inserted row which contains the data.
private String AUTHORITY = "content://com.zebra.securestoragemanager.securecontentprovider/data";
public void insertData() {
Uri cpUri = Uri.parse(AUTHORITY);
ContentValues values = new ContentValues();
// Replace app signature value "ABSFFSDF… WREWED" with the package name signature (from step 3).
values.put("target_app_package", "{\"pkgs_sigs\": [{\"pkg\":\"" + context.getPackageName() + "\",\"sig\":\"" + "ABSFFSDF… WREWED" + "\"}]}");
values.put("data_name", "unique name to identify data"); // Replace with unique name identifier
values.put("data_value", "any string data/json data"); // Replace with data to insert, in plain text or pre-encrypted (from step 1)
values.put("data_input_form", "1"); // Set to 1 (plain text) or 2 (encrypted) based on whether data is pre-encrypted (from step 1)
values.put("data_output_form", "1"); // Set to 1 (plain text) or 2 (encrypted)
values.put("data_input_encrypted_key","Encrypted Secret Key"); // Replace with encrypted secret key if data is pre-encrypted (from step 1). Remove this line if data is not pre-encrypted.
values.put("data_persist_required", "true"); // Set to true or false
Uri createdRow = getContentResolver().insert(cpUri, values);
Log.d(TAG, "Created row: " + createdRow.toString());
}
Multiple Apps
The sample code below inserts data for access by multiple target apps. The package name and signature of all target apps are stored in json format within the ContentValues object, which is then passed to the update() method. If successful, the return value is the URI of the newly inserted row which contains the data.
private String AUTHORITY = "content://com.zebra.securestoragemanager.securecontentprovider/data";
public void insertDataMultipleApps() {
Uri cpUri = Uri.parse(AUTHORITY);
ContentValues values = new ContentValues();
// For multiple target apps, each "com.ztestapp.clientapp" should be replaced with the app package name.
// Replace each sig value (e.g. "24727AB4E6C4A0BE……27B3C") with the package name signature (from step 3).
values.put("target_app_package", "{\"pkgs_sigs\":[
{\"pkg\":\"" + com.ztestapp.clientapp1 + "\",\"sig\":\"24727AB4E6C4A0BE……27B3C\"},
{\"pkg\":\"" + com.ztestapp.clientapp2 + "\",\"sig\":\"24727AB4E……872B27B3C\"},
{\"pkg\":\"" + com.ztestapp.clientapp3 + "\",\"sig\":\"25677AB4E6C……72B27B3C\"}
]}")
values.put("data_name", "unique name to identify data"); // Replace with unique name identifier
values.put("data_value", "any string data/json data"); // Replace with data to insert, in plain text or pre-encrypted (from step 1)
values.put("data_input_form", "1"); // Set to 1 (plain text) or 2 (encrypted) based on whether data is pre-encrypted (from step 1)
values.put("data_output_form", "1"); // Set to 1 (plain text) or 2 (encrypted)
values.put("data_input_encrypted_key","Encrypted Secret Key"); // Replace value with encrypted secret key if data is pre-encrypted (from step 1). Remove this line if data is not pre-encrypted.
values.put("data_persist_required", "true"); // Set to true or false
Uri createdRow = getContentResolver().insert(cpUri, values);
Log.d(TAG, "Created row: " + createdRow.toString());
}
Update Data
Use update() to securely update existing data stored in SSM. The procedure varies depending on whether the data is updated for single or multiple target apps. The sample code demonstrates how to update data received in plain text. Rather than plain text, data can be inserted in pre-encrypted form for added security, in which case the pre-encrypted data and encrypted secret key is passed to the method; see Step 1: Pre-encrypt data. If successful, the value returned is "1".
Single App
The sample code below inserts data for access by a single target app.
private String AUTHORITY = "content://com.zebra.securestoragemanager.securecontentprovider/data";
public void updateData() {
Uri cpUri = Uri.parse(AUTHORITY);
ContentValues values = new ContentValues();
// Replace app signature value "ABSFFSDF… WREWED" with the package name signature (from step 3).
values.put("target_app_package", "{\"pkgs_sigs\": [{\"pkg\":\"" + context.getPackageName() + "\",\"sig\":\"" + "ABSFFSDF… WREWED" + "\"}]}");
values.put("data_name", "unique name to identify data"); // Replace with unique name identifier
values.put("data_value", "any string data/json data"); // Replace with data to update, in plain text or pre-encrypted (from step 1)
values.put("data_input_form", "1"); // Set to 1 (plain text) or 2 (encrypted) based on whether data is pre-encrypted (from step 1)
values.put("data_output_form", "1"); // Set to 1 (plain text) or 2 (encrypted) based on whether data is pre-encrypted (from step 1)
values.put("data_input_encrypted_key","Encrypted Secret Key"); // Replace value with encrypted secret key if data is pre-encrypted (from step 1). Remove this line if data is not pre-encrypted.
values.put("data_persist_required", "true"); // Set to true or false
int rowNumbers = getContentResolver().update(cpUri, values, null, null);
Log.d(TAG, "Records updated: " + rowNumbers);
if (rowNumbers > 0) {
Toast.makeText(mContext, "User info updated", Toast.LENGTH_SHORT).show();
resultView.setText("Query Result");
} else {
Toast.makeText(mContext, "No records to update", Toast.LENGTH_SHORT).show();
}
}
Multiple Apps
The sample code below updates data being accessed by multiple target apps. The package name and signature of each target app are stored in json format within the ContentValues object, which is then passed to the update() method.
private String AUTHORITY = "content://com.zebra.securestoragemanager.securecontentprovider/data";
public void updateDataMultipleApps() {
Uri cpUri = Uri.parse(AUTHORITY);
ContentValues values = new ContentValues();
// For multiple target apps, each "com.ztestapp.clientapp" should be replaced with the app package name.
// Replace each sig value (e.g. "24727AB4E6C4A0BE……27B3C") with the package name signature.
values.put("target_app_package", "{\"pkgs_sigs\":[
{\"pkg\":\"" + com.ztestapp.clientapp1 + "\",\"sig\":\"24727AB4E6C4A0BE……27B3C\"},
{\"pkg\":\"" + com.ztestapp.clientapp2 + "\",\"sig\":\"24727AB4E……872B27B3C\"},
{\"pkg\":\"" + com.ztestapp.clientapp3 + "\",\"sig\":\"25677AB4E6C……72B27B3C\"}
]}")
// Dummy sig is placed here. use SigTool to get this base64 String.
values.put("data_name", "unique name to identify data"); // Replace with unique name identifier
values.put("data_value", "any string data/json data"); // Replace with data to update
values.put("data_input_form", "1"); // Set to 1 (plain text) or 2 (encrypted) based on whether data is encrypted with Data Encryption Tool
values.put("data_output_form", "1"); // Set to 1 (plain text) or 2 (encrypted)
values.put("data_input_encrypted_key","Encrypted Secret Key"); // Replace value with encrypted secret key if data is pre-encrypted. Remove this line if data is not pre-encrypted.
values.put("data_persist_required", "true"); // Set to true or false
int rowNumbers = getContentResolver().update(cpUri, values, null, null);
Log.d(TAG, "Records updated: " + rowNumbers);
if (rowNumbers > 0) {
Toast.makeText(mContext, "User info updated", Toast.LENGTH_SHORT).show();
resultView.setText("Query Result");
} else {
Toast.makeText(mContext, "No records to update", Toast.LENGTH_SHORT).show();
}
}
Query Data
To query data, data information is retrieved based on the target app package name. Call query() to return the cursor object which contains the data parameters (e.g. data_name, data_value, etc.) of the target app package. Traverse the cursor object to individually retrieve the data parameters.
Sample code to query data:
private String AUTHORITY = "content://com.zebra.securestoragemanager.securecontentprovider/data";
public void queryData(){
Uri cpUriQuery = Uri.parse(AUTHORITY + "/[" + context.getPackageName() + "]");
String selection = "target_app_package" + " = '" + context.getPackageName() + "'" + " AND " + "data_persist_required" + " = '" + true + "'";
Cursor cursor = null;
try {
cursor = getContentResolver().query(cpUriQuery, null, selection, null, null);
}
catch (Exception e) {
Log.d(TAG, "Error: " + e.getMessage());
}
// Then traverse the cursor object to retrieve the data fields.
}
Delete Data
delete() API removes the target app package name and all related data information from SSM. If successful, it returns "1". To delete the app package and its data fields:
private String AUTHORITY = "content://com.zebra.securestoragemanager.securecontentprovider/data";
public void deleteData(){
Uri cpUriDelete = Uri.parse(AUTHORITY + "/[" + context.getPackageName() + "]");
String selection = "target_app_package" + " = '" + context.getPackageName() + "'" + " AND " + "data_persist_required" + " = '" + true + "'";
int rowsDeleted = getContentResolver().delete(cpUriDelete, whereClause, null);
if (rowsDeleted > 0) {
Toast.makeText(mContext, "User Info Deleted", Toast.LENGTH_SHORT).show();
}
else {
Toast.makeText(mContext, "No User Info to be deleted", Toast.LENGTH_SHORT).show();
}
}
Sample App
See Data Sharing sample app.