Privacy and data formats
Add privacy functions that coarsen sensitive data, and transformers that convert data to a format such as Open mHealth.
CARP Mobile Sensing (CAMS) runs each measurement's data through data transformers, functions that take a Data object and return one. It does so before it stores the measurement or passes it to the app.
A sampling package supplies transformers for its own data types: privacy functions protect sensitive values, and format transformers convert data to another data format. The example package adds two privacy functions that round air pressure to whole hectopascals (hPa).
CAMS groups transformers in schemas, one for each namespace, keyed by data type. It applies two schemas to every measurement, in this order:
| Schema | Selected by | Default |
|---|---|---|
| Privacy schema | privacySchemaName of the protocol | None: data is kept as collected |
| Data format | dataFormat of the protocol's data endpoint | NameSpace.CARP: data is kept as collected |
Each schema looks up the transformer for the type of the data and leaves data without one unchanged. For how a study protocol selects the schemas, see Data transformation and privacy.
Write a privacy function
A privacy function is a DataTransformer, that is, a Data Function(Data). Replace the contents of lib/barometer_privacy.dart with the two privacy functions of the example package, which round air pressure to whole hectopascals:
One floor of a building is about 3 m high, or 0.36 hPa, so raw readings can tell floors apart. Weather changes of several hectopascals a day stay visible after rounding.
- Test the class with
is, and return any other data unchanged.carp_connectivity_packageasserts the class and then casts. In a release build, where asserts are off, the cast throws aTypeErrorif data of another class ever has the same type. - Change the object in place or return a new one: CAMS replaces the measurement's data with the result.
Register the privacy functions
The example package adds its functions to the default privacy schema in onRegister() of lib/barometer_package.dart, keyed by the measure types:
DataTransformerSchemaRegistryregisters the defaultPrivacySchemaas soon as it is created, solookup()always finds it.add()replaces a function that was added for the same type before.- The key must be the data's type, which is the data class's
jsonType. CAMS looks a function up by the type of the data, not of the measure. The data classes of the example package return the measure types fromjsonType, so the two are the same. - Never register a new
PrivacySchema:register()replaces the schema of that name, and the functions that the packages added to it are gone.
Turn on privacy in the protocol
Privacy functions run only in studies whose protocol names the privacy schema:
Without privacySchemaName, CAMS uses the CARP schema, which has no transformers, and stores the data as collected. Say so in your README, and list the fields that each function changes. The READMEs of carp_communication_package and carp_connectivity_package describe their privacy protection but do not say that it needs privacySchemaName.
The privacySchemaName and the dataFormat must name registered schemas. CAMS looks both up with a null check, so an unknown name makes the transformation throw for every measurement.
Make privacy functions safe to repeat
CAMS transforms a measurement once for every listener of the study's measurement stream, each time on the same Measurement object. The data manager is one listener, and an app that listens to SmartPhoneClientManager().measurements adds another. A privacy function therefore often runs twice on the same data, and a package cannot prevent it.
Write functions that give the same result when they run again. Rounding does, and the AirPressure test in test/package_test.dart of the example package applies the default schema twice to check it:
Hashing does not give the same result: a second run hashes the hash, so the data manager and the app can get different values.
Limits of privacy functions
- Hashing is not anonymization: an unsalted SHA-1 hash of a phone number, as
carp_communication_packagestores, can be reversed by hashing every possible number and comparing. Prefer removing or coarsening a value, as the example package does, and do not call hashed data anonymous. - Only the measurement's own data: CAMS transforms the data of each measurement by its type, not the data nested in it. A user task's result can be stored twice: as a measurement and inside the
CompletedAppTaskof its task, as the survey package does. A function keyed by the result's type protects only the first copy.
Convert data to another format
A format transformer converts data to a data class in another namespace. The registry creates a schema for each of these namespaces:
| Schema | Namespace | Holds |
|---|---|---|
CARPTransformerSchema | dk.cachet.carp (NameSpace.CARP) | No transformers: the default data format |
OMHTransformerSchema | org.openmhealth (NameSpace.OMH) | Open mHealth (OMH) transformers that packages add |
FHIRTransformerSchema | org.hl7.fhir (NameSpace.FHIR) | Fast Healthcare Interoperability Resources (FHIR) transformers that packages add |
PrivacySchema | default-privacy-schema (PrivacySchema.DEFAULT) | Privacy functions that packages add |
The example below converts the readings of a blood pressure package to OMH data points of the openmhealth_schemas package. It is built like the location and activity transformers of the context package. The Movisens package converts heart rate and step counts the same way, and adds a FHIR transformer for heart rate.
BloodPressureReading is the package's data class, with systolic and diastolic in millimeters of mercury (mmHg), for the measure type com.example.bloodpressure.
Adding a format takes four steps:
Add the format's package
Import it in the library file. It also defines
Acceleration,HeartRate,MeasureandStepCount, whichcarp_coredefines too, so hide them:Write the data class of the format
Its
jsonTypeputs the data in the OMH namespace, and its transformer returns any other data unchanged, like a privacy function.Add the transformer in onRegister()
Add to the schema that the registry created. Never register
OMHTransformerSchemaagain:register()replaces the schema with an empty one, and the transformers that other packages added are gone.Select the format in the protocol
A blood pressure measurement after the OMH transformer, as the data manager stores it:
openmhealth_schemas writes the header's creation_date_time in the phone's local time, without an offset.
Effects of a data format
- Data types without a transformer in the schema stay in the CARP format.
- Transformed data has the type
org.openmhealth.data-point, and the data manager stores it under that type. - A
SamplingEventTriggerandmeasurementsByType()see the transformed data, so they no longer match it to the original measure type. CarpDataManageruploads data to CARP Web Services (CAWS) only when its type is a measure type of the deployment, which transformed data does not have. Use a data format only with a data manager that keeps the data on the phone, such as the SQLite or file data manager.- Every OMH data point has the same type, so
FromJsonFactorycan hold only one class for it. Neither CAMS nor the context package registers one, soMeasurement.fromJson()cannot read stored OMH data back. - The privacy schema runs first, so a format transformer gets data that the privacy functions have already changed.
Next step
Testing and publishing
Test the package, run it in an app and publish it.