Key management and data recovery
The Cardinal SDK uses end-to-end encryption to protect your end-users' data, ensuring that only the original creator and their authorized users can decrypt it. Neither the Cardinal team nor you will be able to access the encrypted data unless explicitly authorized by the creator.
A drawback of this approach, however, is that if the user loses their key, they will also lose access to their data, and neither you nor the cardinal team can help to recover it.
When using the Cardinal SDK you should educate the user on the importance of their key, and provide backup and recovery solutions to prevent catastrophic loss of data. The Cardinal SDK provides support for different backup and recovery options so that you can use the best solution for your application.
In general, there are three categories of approaches you can take:
- Create backup of the user keys
- Share data with multiple users and ask for access back in case of key loss
- Use the notary system to safely share a backup of the key with other people
If a user loses their key and can't get it back through a backup or through notaries, any data they've created and haven't shared with other users will be permanently lost.
Creating and using key backups​
The Cardinal SDK automatically stores and retrieves keys using the (key) storage facade provided during initialization. However, if the storage is cleared, you will need to recover the user's keys before they can access their data. This could occur, for instance, if you're building a web app that uses local storage, and the user deletes their browsing data. In mobile or desktop applications, this might happen if the user changes their device.
To prevent data loss, we recommend including a solution in your application to export/backup and recover user keys before release.
Exporting available keys​
You can use the currentDataOwnerKeys
method from the crypto api to access all the keys for the current data owner
available to the SDK.
If you're using hierarchical data owners the SDK will also give all keys available
for parent data owners.
The method returns the keys as a map data owner id (self or parent) → key fingerprint → private key in pkcs8 format
.
You can export the keys any way you think fits your application, but we suggest keeping this structure as it will
simplify the re-import process.
- Kotlin
- Typescript
On kotlin/jvm we can encode the map as json, encoding the pkcs8 key bytes as base64, then save it to a file
import com.icure.cardinal.sdk.CardinalSdk
import com.icure.cardinal.sdk.utils.base64Encode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.FileWriter
suspend fun exportKeysToFile(
sdk: CardinalSdk,
filename: String
) {
val encodedKeys = sdk.crypto.currentDataOwnerKeys().mapValues { (_, keysByFingerprint) ->
keysByFingerprint.mapValues { (_, pkcs8Key) ->
pkcs8Key.base64Encode()
}
}.let { Json.encodeToString(it) }
withContext(Dispatchers.IO) {
FileWriter(filename).use { fw -> fw.write(encodedKeys) }
}
}
On browser you can export the keys as json and let the user download them as a file.
import { CardinalSdk, base64Encode } from "@icure/cardinal-sdk";
async function exportAndDownloadKeysFile(sdk: CardinalSdk) {
const keysJson = JSON.stringify(
await sdk.crypto.currentDataOwnerKeys(),
(key, value) => {
if (value instanceof Int8Array) {
return base64Encode(value)
} else return value
}
)
const blob = new Blob([keysJson], { type: 'application/octet-stream' });
const url = window.URL.createObjectURL(blob);
window.open(url);
}
Restore keys backups​
During initialization, the Cardinal SDK automatically checks if all the keys of the current data owner (and parents) are
available.
If one or more keys are missing, the SDK will try to use the CryptoStrategies
to recover any missing keys.
In this how-to we cover only the basic principles of key recovery. We recommend reading the crypto strategies documentation to have a better understanding about how to customize the way the Cardinal SDK manages the user keys and handles end-to-end encryption.
You can provide custom crypto strategies to the api initialization method that allow the user to re-import keys exported using a past instance of the SDK.
- Kotlin
- Typescript
On kotlin/jvm we ask the user to provide the file with the exported keys and load it.
import com.icure.cardinal.sdk.CardinalSdk
import com.icure.cardinal.sdk.auth.UsernamePassword
import com.icure.cardinal.sdk.crypto.CryptoStrategies
import com.icure.cardinal.sdk.crypto.KeyPairRecoverer
import com.icure.cardinal.sdk.model.specializations.Base64String
import com.icure.cardinal.sdk.model.specializations.KeypairFingerprintV1String
import com.icure.cardinal.sdk.options.AuthenticationMethod
import com.icure.cardinal.sdk.options.SdkOptions
import com.icure.cardinal.sdk.storage.impl.FileStorageFacade
import com.icure.cardinal.sdk.utils.decode
import com.icure.kryptom.crypto.CryptoService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import java.io.File
class MyCryptoStrategies : CryptoStrategies {
override suspend fun recoverAndVerifySelfHierarchyKeys(
keysData: List<CryptoStrategies.KeyDataRecoveryRequest>,
cryptoPrimitives: CryptoService,
keyPairRecoverer: KeyPairRecoverer
): Map<String, CryptoStrategies.RecoveredKeyData> {
// Load the keys from the exported keys file
val backupFile = promptUserForBackupFile()
val recoveredKeys = if (backupFile != null) {
Json.decodeFromString<Map<String, Map<KeypairFingerprintV1String, Base64String>>>(
withContext(Dispatchers.IO) { backupFile.readText() }
)
} else emptyMap()
// If any of the unavailable keys are in exported keys file return them
return keysData.associate { recoveryRequest ->
val dataOwner = recoveryRequest.dataOwnerDetails.dataOwner
dataOwner.id to CryptoStrategies.RecoveredKeyData(
recoveredKeys = recoveryRequest.unavailableKeys.mapNotNull { unavailableKeyInfo ->
val fp = unavailableKeyInfo.publicKey.fingerprintV1()
recoveredKeys[dataOwner.id]?.get(fp)?.let { recoveredKeyB64 ->
fp to cryptoPrimitives.rsa.loadKeyPairPkcs8(
unavailableKeyInfo.keyAlgorithm,
recoveredKeyB64.decode()
)
}
}.toMap(),
keyAuthenticity = emptyMap()
)
}
// No need to save recovered keys: they will automatically be saved to the storage
}
private suspend fun promptUserForBackupFile(): File? {
TODO("Implement this yourself")
}
}
suspend fun initializeSdk(
username: String,
password: String
): CardinalSdk = CardinalSdk.initialize(
"MyApplication",
"https://api.icure.cloud",
AuthenticationMethod.UsingCredentials(UsernamePassword(username, password)),
FileStorageFacade("/path/to/key/storage"),
// Provide the crypto strategies with the recovery method
SdkOptions(cryptoStrategies = MyCryptoStrategies())
)
On browser you can ask the user to upload the file with the previously exported keys.
import {
CryptoStrategies,
KeyPairRecoverer,
SpkiHexString,
XCryptoService,
XRsaKeypair,
KeypairFingerprintV1String,
spkiHexKeyToFingerprintV1,
RecoveryResult,
CardinalSdk,
AuthenticationMethod,
StorageFacade,
DataOwnerWithType,
base64Decode
} from "@icure/cardinal-sdk";
class MyCryptoStrategies extends CryptoStrategies {
async recoverAndVerifySelfHierarchyKeys(
keysData: Array<CryptoStrategies.KeyDataRecoveryRequest>,
cryptoPrimitives: XCryptoService,
keyPairRecoverer: KeyPairRecoverer
): Promise<{[dataOwnerId: string]: CryptoStrategies.RecoveredKeyData}> {
let recoveredResult: RecoveryResult<{ [dataOwnerId: string]: { [pub: SpkiHexString]: XRsaKeypair } }>
const recoveredFromFile = await this.loadKeysFromFile(await this.askKeyBackupFile())
// If any of the unavailable keys are in exported keys file return them
const result: { [dataOwnerId: string]: CryptoStrategies.RecoveredKeyData } = {}
for (const recoveryRequest of keysData) {
const dataOwner = recoveryRequest.dataOwnerDetails.dataOwner
const currDataOwnerRecoveredData = recoveredFromFile[dataOwner.id]
const currRecoveryResult: { [fp: KeypairFingerprintV1String]: XRsaKeypair } = {}
if (currDataOwnerRecoveredData != undefined) {
for (const unavailableKeyInfo of recoveryRequest.unavailableKeys) {
const fingerprint = spkiHexKeyToFingerprintV1(unavailableKeyInfo.publicKey)
const recoveredKey = currDataOwnerRecoveredData[fingerprint]
if (recoveredKey != undefined) {
currRecoveryResult[fingerprint] = await cryptoPrimitives.rsa.loadKeyPairPkcs8(
unavailableKeyInfo.keyAlgorithm,
base64Decode(recoveredKey)
)
}
}
}
result[dataOwner.id] = {
recoveredKeys: currRecoveryResult,
keyAuthenticity: {}
}
}
// No need to save recovered keys: they will automatically be saved to the storage
return result
}
async loadKeysFromFile(
file: File
): Promise<{ [dataOwnerId: string ]: { [keyFp: KeypairFingerprintV1String]: string }}> {
const content = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result === 'string') {
resolve(reader.result);
} else {
reject(new Error('Unexpected result type'));
}
};
reader.onerror = () => {
reject(new Error('Error reading file'));
};
reader.readAsText(file);
})
let contentJson: any
try {
contentJson = JSON.parse(content)
} catch {
throw new Error("Invalid key backup file")
}
if (
typeof contentJson !== 'object' ||
Object.values(contentJson).some((dataOwnerKeys: any) => {
return typeof dataOwnerKeys !== 'object' || Object.values(dataOwnerKeys).some((key) => {
return typeof key !== 'string'
})
})
) throw new Error("Invalid key backup file")
return contentJson
}
async askKeyBackupFile(): Promise<File> {
throw new Error("Implement this yourself")
}
}
async function initializeSdk(
username: string,
password: string
) {
const sdk = await CardinalSdk.initialize(
"MyApplication",
"https://api.icure.cloud",
new AuthenticationMethod.UsingCredentials.UsernameLongToken(username, password),
StorageFacade.usingBrowserLocalStorage(),
// Provide the crypto strategies with the recovery method
{ cryptoStrategies: new MyCryptoStrategies() }
)
}
Export newly created key​
During initialization, the SDK ensures that the logged user has at least a key available. If the user doesn't have a key, or if none of their keys was found in the storage or recovered by the crypto strategies, the SDK needs to create a new key.
If this happened, after the rest of the SDK is successfully initialized, the method notifyNewKeyCreated
will be called
with the newly created key.
You can use this method to prompt the user to export key.
The SDK can only create a new key for the current data owner. If you're using hierarchical data owners, and there is no key available for one of the parent data owners, the SDK initialization will fail.
- Kotlin
- Typescript
import com.icure.cardinal.sdk.CardinalApis
import com.icure.cardinal.sdk.crypto.CryptoStrategies
import com.icure.kryptom.crypto.CryptoService
import com.icure.kryptom.crypto.RsaAlgorithm
import com.icure.kryptom.crypto.RsaKeypair
class MyCryptoStrategies : CryptoStrategies {
override suspend fun notifyNewKeyCreated(
apis: CardinalApis,
key: RsaKeypair<RsaAlgorithm.RsaEncryptionAlgorithm.OaepWithSha256>,
cryptoPrimitives: CryptoService
) {
promptUserForNewExportedKeysFilePath()?.also { filename ->
// Like previous example
val encodedKeys = apis.crypto.currentDataOwnerKeys().mapValues { (_, keysByFingerprint) ->
keysByFingerprint.mapValues { (_, pkcs8Key) ->
pkcs8Key.base64Encode()
}
}.let { Json.encodeToString(it) }
withContext(Dispatchers.IO) {
FileWriter(filename).use { fw -> fw.write(encodedKeys) }
}
}
}
suspend fun promptUserForNewExportedKeysFilePath(): String? =
TODO("Implement this yourself")
}
import {
CryptoStrategies,
XCryptoService,
XRsaKeypair,
CardinalApis, base64Encode
} from "@icure/cardinal-sdk";
class MyCryptoStrategies extends CryptoStrategies {
async notifyNewKeyCreated(apis: CardinalApis, key: XRsaKeypair, cryptoPrimitives: XCryptoService): Promise<void> {
if (await this.askUserToDownloadNewKey()) {
// Like previous example
const keysJson = JSON.stringify(
await apis.crypto.currentDataOwnerKeys(),
(key, value) => {
if (value instanceof Int8Array) {
return base64Encode(value)
} else return value
}
)
const blob = new Blob([keysJson], { type: 'application/octet-stream' });
const url = window.URL.createObjectURL(blob);
window.open(url);
}
}
askUserToDownloadNewKey(): Promise<boolean> {
throw new Error("Implement this yourself")
}
}
Keys backup representation​
Exporting the keys of the user as files, like shown in the previous examples, can work in desktop applications. However, managing files on a mobile device is often a cumbersome operation. A more user-friendly solution for mobile applications could be to use QR codes: you could allow your users to instantly recover keys using a QR code generated in the past, or generated on-the-fly by another device that has the key. However, if you try to generate a QR code for an RSA key, you can see that the resulting code is quite big, and older or low-end devices may have troubles reading it.
To help with this issue, you can use the recovery data system of the Cardinal SDK. The recovery data system allows storing encrypted data on the Cardinal backend using a small encryption key (AES-128 or AES-256 → 16 or 32 bytes). Your user can create recovery data containing his private keys and save the encryption key for the recovery data somewhere. At a later moment, if the user loses their private keys, they can login and use the recovery data key to get them back.
This way, instead of having to provide all the missing keys the user has to provide only the recovery data key. This allows recovery with smaller QR codes, or even with manual input if you encode the recovery key using human-friendly representation such as base32.
- Kotlin
- Typescript
import com.icure.cardinal.sdk.CardinalSdk
import com.icure.cardinal.sdk.crypto.entities.RecoveryKeySize
suspend fun createRecoveryDataAndPrintKey(
sdk: CardinalSdk
) {
val recoveryKey = sdk.recovery.createRecoveryInfoForAvailableKeyPairs(
includeParentsKeys = true,
recoveryKeySize = RecoveryKeySize.Bytes32,
)
println("Your new recovery key is: ${recoveryKey.asBase32().chunked(4).joinToString("-")}")
// Your new recovery key is: UALR-WKWD-NWLX-AWOK-KRUQ-G42Q-WUPS-EOHC-TMPP-CXP7-HDDV-XFAE-LWGQ
}
import {
CardinalSdk,
RecoveryKeySize
} from "@icure/cardinal-sdk";
async function createRecoveryDataAndPrintKey(sdk: CardinalSdk): Promise<void> {
const recoveryKey = await sdk.recovery.createRecoveryInfoForAvailableKeyPairs({
includeParentsKeys: true,
recoveryKeySize: RecoveryKeySize.Bytes32,
});
const formattedKey = recoveryKey.asBase32().match(/.{1,4}/g)?.join('-');
console.log(`Your new recovery key is: ${formattedKey}`);
// Your new recovery key is: UALR-WKWD-NWLX-AWOK-KRUQ-G42Q-WUPS-EOHC-TMPP-CXP7-HDDV-XFAE-LWGQ
}
You can use the recovery data key to directly in the crypto strategies through the keyPairRecoverer
parameter.
- Kotlin
- Typescript
import com.icure.cardinal.sdk.crypto.CryptoStrategies
import com.icure.cardinal.sdk.crypto.KeyPairRecoverer
import com.icure.cardinal.sdk.crypto.entities.RecoveryDataKey
import com.icure.cardinal.sdk.crypto.entities.RecoveryResult
import com.icure.cardinal.sdk.model.specializations.SpkiHexString
import com.icure.kryptom.crypto.CryptoService
import com.icure.kryptom.crypto.RsaAlgorithm
import com.icure.kryptom.crypto.RsaKeypair
class MyCryptoStrategies : CryptoStrategies {
override suspend fun recoverAndVerifySelfHierarchyKeys(
keysData: List<CryptoStrategies.KeyDataRecoveryRequest>,
cryptoPrimitives: CryptoService,
keyPairRecoverer: KeyPairRecoverer
): Map<String, CryptoStrategies.RecoveredKeyData> {
var recoveryResult: RecoveryResult<Map<String, Map<SpkiHexString, RsaKeypair<RsaAlgorithm.RsaEncryptionAlgorithm>>>>
do {
recoveryResult = keyPairRecoverer.recoverWithRecoveryKey(
RecoveryDataKey.fromBase32(promptUserForRecoveryKey()),
autoDelete = true // true -> the recovery data to be deleted after use
)
} while (!recoveryResult.isSuccess)
return keysData.associate { recoveryRequest ->
val dataOwner = recoveryRequest.dataOwnerDetails.dataOwner
dataOwner.id to CryptoStrategies.RecoveredKeyData(
recoveredKeys = recoveryRequest.unavailableKeys.mapNotNull { unavailableKeyInfo ->
val pub = unavailableKeyInfo.publicKey
recoveryResult.value[dataOwner.id]?.get(pub)?.let { recoveredKey ->
pub.fingerprintV1() to recoveredKey
}
}.toMap(),
keyAuthenticity = emptyMap()
)
}
}
private suspend fun promptUserForRecoveryKey(): String {
TODO("Implement this yourself")
}
}
import {
CryptoStrategies,
KeyPairRecoverer,
SpkiHexString,
RecoveryDataKey,
XCryptoService,
XRsaKeypair,
KeypairFingerprintV1String,
spkiHexKeyToFingerprintV1,
RecoveryResult
} from "@icure/cardinal-sdk";
class MyCryptoStrategies extends CryptoStrategies {
async recoverAndVerifySelfHierarchyKeys(
keysData: Array<CryptoStrategies.KeyDataRecoveryRequest>,
cryptoPrimitives: XCryptoService,
keyPairRecoverer: KeyPairRecoverer
): Promise<{[dataOwnerId: string]: CryptoStrategies.RecoveredKeyData}> {
let recovered: RecoveryResult<{ [dataOwnerId: string]: { [pub: SpkiHexString]: XRsaKeypair } }>
do {
recovered = await keyPairRecoverer.recoverWithRecoveryKey(
RecoveryDataKey.fromBase32(await this.promptUserForRecoveryKey()),
false
)
} while (recovered instanceof RecoveryResult.Failure)
const result: { [dataOwnerId: string]: CryptoStrategies.RecoveredKeyData } = {}
for (const recoveryRequest of keysData) {
const dataOwner = recoveryRequest.dataOwnerDetails.dataOwner
const currDataOwnerRecoveredData = recovered.data[dataOwner.id]
const currRecoveryResult: { [fp: KeypairFingerprintV1String]: XRsaKeypair } = {}
if (currDataOwnerRecoveredData != undefined) {
for (const unavailableKeyInfo of recoveryRequest.unavailableKeys) {
const recoveredKey = currDataOwnerRecoveredData[unavailableKeyInfo.publicKey]
if (recoveredKey != undefined) {
currRecoveryResult[spkiHexKeyToFingerprintV1(unavailableKeyInfo.publicKey)] = recoveredKey
}
}
}
result[dataOwner.id] = {
recoveredKeys: currRecoveryResult,
keyAuthenticity: {}
}
}
return result
}
private async promptUserForRecoveryKey(): Promise<string> {
throw new Error("Implement this yourself")
}
}
Multi-device key management​
If a user is using the cardinal SDK from multiple devices, you should make sure the same private keys are available on both devices. If the same user is logged in on two different devices with different available private keys, the data available on a device could be different from the data available on the other.
To prevent this situation from happening, we suggest that you provide a simple way to copy keys from a device to another. For example, you could use the recovery data mechanism explained previously to create some short-lived key recovery data.
- Kotlin
- Typescript
import com.icure.cardinal.sdk.CardinalSdk
import com.icure.cardinal.sdk.crypto.entities.RecoveryKeySize
suspend fun createShortRecoveryDataAndPrintKey(
sdk: CardinalSdk
) {
val recoveryKey = sdk.recovery.createRecoveryInfoForAvailableKeyPairs(
includeParentsKeys = true,
// We want to use a shorter recovery data key to simplify the transfer
recoveryKeySize = RecoveryKeySize.Bytes16,
// Since we're creating this recovery data only to transfer keys between devices,
// we should give it a limited lifespan
lifetimeSeconds = 5 * 60
)
println("Your temporary recovery key is: ${recoveryKey.asBase32().chunked(4).joinToString("-")}")
// Your temporary recovery key is: NQ2I-HTJ7-Y3CK-GUIH-EMKK-M7CY-HA
}
import {
CardinalSdk,
RecoveryKeySize
} from "@icure/cardinal-sdk";
async function createShortRecoveryDataAndPrintKey(sdk: CardinalSdk): Promise<void> {
const recoveryKey = await sdk.recovery.createRecoveryInfoForAvailableKeyPairs({
includeParentsKeys: true,
// We want to use a shorter recovery data key to simplify the transfer
recoveryKeySize: RecoveryKeySize.Bytes16,
// Since we're creating this recovery data only to transfer keys between devices,
// we should give it a limited lifespan
lifetimeSeconds: 5 * 60
});
const formattedKey = recoveryKey.asBase32().match(/.{1,4}/g)?.join('-');
console.log(`Your temporary recovery key is: ${formattedKey}`);
// Your temporary recovery key is: NQ2I-HTJ7-Y3CK-GUIH-EMKK-M7CY-HA
}
Regaining access to shared data after key loss​
If the user has completely lost their key, and can't recover it, they could still be able to regain access to their data if they had previously shared it with other users.
The user will have to create a new keypair during the initialization of the SDK, then notify the other users that he shared data with (or that shared data with him) that he needs access back to the entities. We call this a "give access back" request. You can do this using some external method that suits your application, or internally to the Cardinal SDK by using maintenance tasks.
- Kotlin
- Typescript
import com.icure.cardinal.sdk.CardinalSdk
import com.icure.cardinal.sdk.crypto.CryptoStrategies
import com.icure.cardinal.sdk.crypto.impl.exportSpkiHex
import com.icure.cardinal.sdk.model.extensions.publicKeysSpki
import com.icure.kryptom.crypto.CryptoService
import com.icure.kryptom.crypto.RsaAlgorithm
import com.icure.kryptom.crypto.RsaKeypair
class MyCryptoStrategies : CryptoStrategies {
override suspend fun notifyNewKeyCreated(
sdk: CardinalSdk,
key: RsaKeypair<RsaAlgorithm.RsaEncryptionAlgorithm.OaepWithSha256>,
cryptoPrimitives: CryptoService
) {
val pubKeyHex = cryptoPrimitives.rsa.exportSpkiHex(key.public)
val self = sdk.dataOwner.getCurrentDataOwnerStub().stub
if (self.publicKeysSpki.size > 1) {
// This is not the first key created for the user
// We may have some data shared with other users using a previous key
sdk.cardinalMaintenanceTask.createKeyPairUpdateNotificationsToAllDelegationCounterparts(pubKeyHex)
}
}
}
import {
CardinalSdk,
CryptoStrategies,
XRsaKeypair,
XCryptoService,
hexEncode,
allPublicKeysOf
} from "@icure/cardinal-sdk";
class MyCryptoStrategies extends CryptoStrategies {
override async notifyNewKeyCreated(
sdk: CardinalSdk,
key: XRsaKeypair,
cryptoPrimitives: XCryptoService
) {
const pubKeyHex = hexEncode(await cryptoPrimitives.rsa.exportPublicKeySpki(key.public))
const self = (await sdk.dataOwner.getCurrentDataOwnerStub()).stub
if (allPublicKeysOf(self).length > 1) {
// This is not the first key created for the user
// We may have some data shared with other users using a previous key
await sdk.cardinalMaintenanceTask.createKeyPairUpdateNotificationsToAllDelegationCounterparts(pubKeyHex)
}
}
}
Now the recipient of the give-access-back can respond to it and give back access to the user: this can be done either directly by updating the give-access-back with the new public key of the requester, or indirectly by using the recovery data system.
Both give-access-back solutions don't need to directly update the medical data. Even if you have thousands of contacts and patients with different encryption keys shared between the sender and recipient of the give-access-back request, there is no need to update them to resolve the request.
The only entities updated by the give-access-back request are the metadata entities used to store the exchange keys.
Indirect give-access-back with recovery data​
The recovery data mechanism explained earlier for recovery of private keys can also be used with the give-access-back mechanism. In this case, the recipient of the give-access-back request can create recovery data containing the decrypted exchange keys between the sender and recipient of the request. The recipient can now share the recovery data key with the sender, which the sender can use to access again all exchange keys, and in turn all entities, shared between them.
- Kotlin
- Typescript
import com.icure.cardinal.sdk.CardinalSdk
import com.icure.cardinal.sdk.crypto.entities.RecoveryDataKey
// Used by the recipient of the give-access-back request
suspend fun createRecoveryDataForGiveAccessBackAndPrint(
sdk: CardinalSdk,
dataOwnerWithLostKeyId: String
) {
val recoveryKey = sdk.recovery.createExchangeDataRecoveryInfo(delegateId = dataOwnerWithLostKeyId)
println("Share this recovery key with $dataOwnerWithLostKeyId: ${recoveryKey.asBase32()}")
}
//Used by the sender of the give-access-back after getting a recovery key
suspend fun useRecoveryDataFromGiveAccessBack(
sdk: CardinalSdk,
recoveryKey: String
) {
val recoveryResult = sdk.recovery.recoverExchangeData(RecoveryDataKey.fromBase32(recoveryKey))
if (recoveryResult != null) throw IllegalArgumentException(
"Failed to use recovery key $recoveryKey for reason: $recoveryResult"
)
}
import { CardinalSdk, RecoveryDataKey } from "@icure/cardinal-sdk";
// Used by the recipient of the give-access-back request
async function createRecoveryDataForGiveAccessBackAndPrint(
sdk: CardinalSdk,
dataOwnerWithLostKeyId: string
): Promise<void> {
const recoveryKey = await sdk.recovery.createExchangeDataRecoveryInfo(dataOwnerWithLostKeyId)
console.log(`Share this recovery key with ${dataOwnerWithLostKeyId}: ${recoveryKey.asBase32()}`)
}
//Used by the sender of the give-access-back after getting a recovery key
async function useRecoveryDataFromGiveAccessBack(
sdk: CardinalSdk,
recoveryKey: string
): Promise<void> {
const recoveryResult = await sdk.recovery.recoverExchangeData(RecoveryDataKey.fromBase32(recoveryKey))
if (recoveryResult != undefined) {
throw new Error(`Failed to use recovery key ${recoveryKey} for reason: ${recoveryResult}`)
}
}
When using the exchange data recovery key the SDK will automatically update the exchange data between the sender and recipient of the give-access-back request, so that the sender can access it with their new private key.
Direct give-access-back using new public key​
If the give-access-back request includes the public key of the sender, then the recipient can directly update the exchange keys to allow the sender to access them through their new personal key.
This could be done, for example, if the give-access-back request was done through maintenance tasks:
- Kotlin
- Typescript
import com.icure.cardinal.sdk.CardinalSdk
import com.icure.cardinal.sdk.filters.MaintenanceTaskFilters
import com.icure.cardinal.sdk.model.MaintenanceTask
import com.icure.cardinal.sdk.model.sdk.KeyPairUpdateNotification
import com.icure.cardinal.sdk.utils.pagination.forEach
suspend fun resolveGiveAccessBackRequests(sdk: CardinalSdk) {
var maintenanceTasksToDelete = mutableListOf<MaintenanceTask>()
try {
val selfId = sdk.dataOwner.getCurrentDataOwnerId()
val giveAccessBackRequests = sdk.maintenanceTask.filterMaintenanceTasksBy(
MaintenanceTaskFilters.byTypeForSelf(KeyPairUpdateNotification.TASK_TYPE)
)
giveAccessBackRequests.forEach { rawMaintenanceTask ->
val parsedRequest = KeyPairUpdateNotification.parseFromMaintenanceTask(rawMaintenanceTask)
// The filter could also retrieve maintenance tasks created by the current data owner
if (parsedRequest.concernedDataOwnerId != selfId && promptGiveAccessBackConfirmation(parsedRequest)) {
sdk.cardinalMaintenanceTask.applyKeyPairUpdate(parsedRequest)
maintenanceTasksToDelete.add(rawMaintenanceTask)
}
}
} finally {
// Delete the resolved maintenance tasks so we don't check them next time.
sdk.maintenanceTask.deleteMaintenanceTasks(maintenanceTasksToDelete)
}
}
suspend fun promptGiveAccessBackConfirmation(request: KeyPairUpdateNotification): Boolean {
TODO("Implement this yourself")
}
import {
CardinalSdk,
KeyPairUpdateNotification,
MaintenanceTask,
MaintenanceTaskFilters,
} from "@icure/cardinal-sdk";
async function resolveGiveAccessBackRequests(sdk: CardinalSdk): Promise<void> {
const maintenanceTasksToDelete: MaintenanceTask[] = [];
try {
const selfId = await sdk.dataOwner.getCurrentDataOwnerId();
const giveAccessBackRequests = await sdk.maintenanceTask.filterMaintenanceTasksBy(
MaintenanceTaskFilters.byTypeForSelf(KeyPairUpdateNotification.TASK_TYPE)
);
while (await giveAccessBackRequests.hasNext()) {
const rawMaintenanceTaskBatch = await giveAccessBackRequests.next(100)
for (const rawMaintenanceTask of rawMaintenanceTaskBatch) {
const parsedRequest = KeyPairUpdateNotification.parseFromMaintenanceTask(rawMaintenanceTask);
// The filter could also retrieve maintenance tasks created by the current data owner
if (parsedRequest.concernedDataOwnerId !== selfId && await promptGiveAccessBackConfirmation(parsedRequest)) {
await sdk.cardinalMaintenanceTask.applyKeyPairUpdate(parsedRequest);
maintenanceTasksToDelete.push(rawMaintenanceTask);
}
}
}
} finally {
// Delete the resolved maintenance tasks so we don't check them next time.
await sdk.maintenanceTask.deleteMaintenanceTasks(maintenanceTasksToDelete);
}
}
async function promptGiveAccessBackConfirmation(request: KeyPairUpdateNotification): Promise<boolean> {
throw new Error("Implement this yourself");
}
We strongly recommend using this give-access-back solution only if you're using external solutions to verify the authenticity of the public keys stored in the Cardinal backend.
For more information, we recommend reading the end-to-end encryption explanation page
Notary system​
🚧 This section is under construction 🚧
The notary system is an alternative to key backups. Instead of (or in addition to) a private backup of the key, a user can designate a group of trusted notaries that each will hold a piece of the user's private key. If the user loses his key, he can ask the notaries for the piece of key they were holding and reconstruct the full key.
At its base, this system uses shamir's secret sharing algorithm. This allows also specifying a threshold of pieces needed to reconstruct the keys that is lower than the number of pieces created. This system is safer than directly sharing a key backup or medical data because even if some notaries (depending on the chosen threshold) become untrustworthy, the data remains secure.
The notaries system enables secure data recovery even when the data was never explicitly shared with other users, a critical requirement for some systems. For instance, consider a Cardinal-based EHR application used by a solo GP. The data is end-to-end encrypted, meaning only the GP can read it. If the GP were to go missing, patient data would be lost permanently, which would not only be a disservice to most patients but could also delay treatment to patients with life-threatening conditions. However, if the GP had designated notaries, the private key can be recovered, allowing access to patient data even in this extreme scenario.