Migrating from version 1.10 to version 2.0

See the changelog for a description of the new concepts introduced in the 2.0 release.

Important caveat

When upgrading to the 2.0 SDK, make sure to upgrade both web and mobile applications at the same time.

Otherwise, you may have blocks pushed to the Tanker servers from the web application that mobile applications won't be able to process.

Server-side changes

The first step is to replace the usertoken SDK with the identity SDK.

Then, if you had existing users with user tokens stored in database, you should convert them to secret identities using the function named upgradeUserToken from the identity SDK.

For instance, in Go:


import (
    "github.com/TankerHQ/identity-go/identity"
)

// Note: error handling omitted for brievity
func GetTankerIdentity(userID string) {
    user := storage.GetUser(userID)

    config := identity.Config {
        AppID: ...,
        AppSecret: ...,
    }

    if user.tankerIdentity != "" {
        // Tanker identity already created, return it
        return user.tankerIdentity
    }

    if user.tankerUserToken != "" {
        // user had a user token, convert to an identity
        user.tankerIdentity = identity.UpradeUserToken(config, userID, user.tankerUserToken)
    }

    if user.tankerIdentity != "" {
        // user had neither user token nor identiny, create a new one, save it and
        // return it
        user.tankerIdentity = identity.Create(config, userID)
    }

    storage.SaveUser(user)
    return user.tankerIdentity
}

Once this is done, you may proceed to adapt server code to store and retrieve identities, and, in case you need it, implement pre-registration sharing

In particular, this means you should modify your authentication routes to send back tanker identities instead of user tokens.

Client-side changes

Encryption methods renamed

The following methods have been renamed:

  • encryptDataFromData -> encryptData
  • encryptDataFromString -> encryptString
  • decryptDataFromData -> decryptData

Adapt to new asynchronous model

Let's use decryptStringFromData and stop methods as examples. Here's what the code may look like before the 2.0 version:

// Before 2.0

PMKPromise<NSData *> *promise = [tanker decryptStringFromData:encryptedData];
promise
    .then(^(NSData *clearData){
        // Handle clearData
    })
    .catch(^(NSError *error){
        // Handle error while trying to decrypt
    });

[tanker close]
    .then(^{
        // Tanker session is now  closed
    })
    .catch(^(NSError *error){
        // Handle error while trying to  close the session
    });

There are several strategies to port this code to the 2.0 version

Solution 1: with explicit completionHandler

A first strategy is to use the new completionHandler argument as an explicit callback, like so:

// After 2.0
[self.tanker decryptStringFromData:encryptedData
                 completionHandler:^(NSData *clearData, NSError *error) {
                   if (error != nil) {
                     // Handle error
                   } else {
                     // Handle clearData
                   }
                 }];

[self.tanker stopWithCompletionHandler:^(NSError *error) {
  if (error != nil) {
    // Handle error
  } else {
    // The Tanker session is now closed
  }
}];

Solution 2: with PromiseKit's adapters and resolvers

You may also use an existing asynchronous library to make the code a bit more readable and composable.

For instance, using PromiseKit adapters and resolvers:

// When wrapping a method that has a return value
PMKPromise<NSData *> *promise =
  [PMKPromise promiseWithAdapter:^(PMKAdapter adapter) {
    [tanker decryptStringFromData:encryptedData completionHandler:adapter];
  }];
promise.then(^(NSData* clearData) {
  // Handle clearData
})

// When wrapping a method that does not have a return value
PMKPromise* promise = [PMKPromise promiseWithResolver:^(PMKResolver resolver) {
  [tanker stopWithCompletionHandler:resolver]
];
promise.then(^{
    // Tanker session is now closed
});

Solution 3: use a facade class

You may also use the facade design pattern by creating a class that expose an easier to use API. Here's an example, still using PromiseKit:

// In PMKTanker.h
@interface PMKTanker : NSObject

@property TKRTanker *tanker;

- (PMKPromise<NSData *> *)decryptStringFromData(NSData *encryptedData);
- (PMKPromise *)stop;
// ...

@end

// In PMKTanker.m
@implementation PMKTanker

- (PMKPromise<NSData *> *)decryptStringFromData:(NSData *)clearData {
  return [PMKPromise promiseWithAdapter:^(PMKAdapter adapter) {
    [self.tanker decryptStringFromData:clearData completionHandler:adapter];
  }];
}

- (PMKPromise *)stop {
  return [PMKPromise promiseWithResolver:^(PMKResolver resolver) {
    [self.tanker stopWithCompletionHandler:resolver];
  }];
}

// ...

@end

// In YourApplication.m
PMKTanker tanker = ...;
[tanker decryptStringFromData: encryptedData].then(^{
    // ...
});

[tanker stop].then(^{
    // ...
});

You can find such a facade in the quickstart repository.

Overview of method changes

The old and new API methods do not map 1-to-1, since internal behaviors have been redispatched significantly.

Here is a quick recap table to have a grasp of the changes described in the sections hereafter:

SDK 2.0 methods Roughly equivalent to SDK 1.10 methods
start first half of open
registerIdentity second half of open + registerUnlock with a verified method
verifyIdentity second half of open + unlockCurrentDevice
setVerificationMethod additional registerUnlock call, with a verified method
stop close
SDK 2.0 methods Roughly equivalent to SDK 1.10 methods
start first half of open
registerIdentity second half of open + registerUnlock with a verified method
verifyIdentity second half of open + unlockCurrentDevice
setVerificationMethod additional registerUnlock call, with a verified method
stop close
SDK 2.0 methods Roughly equivalent to SDK 1.10 methods
startWithIdentity first half of openWithUserID
registerIdentityWithVerification second half of openWithUserID + registerUnlock with a verified method
verifyIdentityWithVerification second half of openWithUserID + unlockCurrentDeviceWith...
setVerificationMethod additional registerUnlock call, with a verified method
stopWithCompletionHandler close

Starting a session

// Before 2.0
await tanker.open(userId, userToken);

// After 2.0
await tanker.start(identity);

Note: tanker.open() used to block until the device was unlocked, using the tanker.on('unlockRequired') callback. Now tanker.start() can return even if the Tanker session is not ready. See the section starting a Tanker session for details.

// Before 2.0
tanker.open(userId, userToken);

// After 2.0
tanker.start(identity);

Note: The future returned by tanker.open() used to resolve when the device was unlocked, using the callback given to tanker.connectUnlockRequiredHandler. Now, the future returned by tanker.start() gets resolved even if the Tanker session is not ready. See the section starting a Tanker session for details.

// Before 2.0
[tanker openWithUserID:userId userToken:userToken];

// After 2.0
[tanker startWithIdentity:identity completionHandler:...];

Note: openWithUserID used to block until the device was unlocked, using the callback given to connectUnlockRequiredHandler. Now, startWithIdentity will call the completion handler even if the Tanker session is not ready. See the section starting a Tanker session for details.

Use new Tanker status values

// Before 2.0
const {
  CLOSED,
  OPEN,
  UNLOCK_REQUIRED,
} = tanker;

// After 2.0
const {
  STOPPED,
  READY,
  IDENTITY_REGISTRATION_NEEDED,
  IDENTITY_VERIFICATION_NEEDED
} = Tanker.statuses;
// Before 2.0
enum TankerStatus {
  IDLE,
  OPEN,
  USER_CREATION,
  DEVICE_CREATION,
  CLOSING,
}

// After 2.0
enum Status {
  STOPPED,
  READY,
  IDENTITY_REGISTRATION_NEEDED,
  IDENTITY_VERIFICATION_NEEDED
}
// Before 2.0
enum TKRStatus {
  TKRStatusClosed,
  TKRStatusOpen,
  TKRStatusUserCreation,
  TKRStatusDeviceCreation,
  TKRStatusClosing
};

// After 2.0
enum TKRStatus {
  TKRStatusStopped,
  TKRStatusReady,
  TKRStatusIdentityRegistrationNeeded,
  TKRStatusIdentityVerificationNeeded,
};

Note that you must register a verification method and verify it right away when a user first connects to Tanker. Previously it was possible to create a user without any unlock mechanism in place and thus risk data loss.

Replace registerUnlock with registerIdentity

With email

// Before 2.0
// After open()
await tanker.registerUnlock({ email });

// After 2.0
// After start(), in case status is IDENTITY_REGISTRATION_NEEDED
await tanker.registerIdentity({ email, verificationCode });
// Before 2.0
// After open()
TankerUnlockOptions options = new TankerUnlockOptions();
options.setEmail(email);
tanker.registerUnlock(options);

// After 2.0
// After start(), in case status is IDENTITY_REGISTRATION_NEEDED
Verification verification = new EmailVerification(email, verificationCode);
tanker.registerIdentity(verification);
// Before 2.0
TKRUnlockOptions* options = [TKRUnlockOptions defaultOptions];
options.email = email;
[tanker registerUnlock:options];

// After 2.0
TKRVerification* verification = [TKRVerification verificationFromEmail:email code:verificationCode];
[tanker registerIdentityWithVerification:verification completionHandler:...];

Note

You need to send an email containing the verification code before using other Tanker methods.

With password

Replace password with passphrase:

// Before 2.0
// After open()
await tanker.registerUnlock({ password });

// After 2.0
// After start(), in case status in IDENTITY_REGISTRATION_NEEDED
await tanker.registerIdentity({ passphrase: password });
// Before 2.0
// After open()
TankerUnlockOptions options = new TankerUnlockOptions();
options.setPassword(password);
tanker.registerUnlock(options);

// After 2.0
// After start(), in case status in IDENTITY_REGISTRATION_NEEDED
Verification verification = new PassphraseVerification(passphrase);
tanker.registerIdentity(verification);
// Before 2.0
TKRUnlockOptions* options = [TKRUnlockOptions defaultOptions];
options.password = password;
[tanker registerUnlock:options];

// After 2.0
TKRVerification* verification = [TKRVerification verificationFromPassphrase:passphrase];
[tanker registerIdentityWithVerification:verification completionHandler:...];

Replace unlockCurrentDevice with verifyIdentity

With email

// Before 2.0
// After open(), in the unlockRequired callback:
await tanker.unlockCurrentDevice({ verificationCode });

// After 2.0
// After start(), in case status in IDENTITY_VERIFICATION_NEEDED
await tanker.verifyIdentity({ email, verificationCode });
// Before 2.0
// After open()
tanker.unlockCurrentDeviceWithVerificationCode(verificationCode);

// After 2.0
// After start(), in case status is IDENTITY_REGISTRATION_NEEDED
Verification verification = new EmailVerification(email, verificationCode);
tanker.verifyIdentity(verification);
// Before 2.0
// After openWithUserID
[tanker unlockCurrentDeviceWithVerificationCode:verificationCode];

// After 2.0
TKRVerification* verification = [TKRVerification verificationFromEmail:email code:verificationCode];
// After startWithIdentity, in case status is TKRStatusIdentityRegistrationNeeded
[tanker verifyIdentityWithVerification:verification completionHandler:...];

With password

// Before 2.0
// After open(), in the unlockRequired callback:
await tanker.unlockCurrentDevice({ password });

// After 2.0
// After start(), in case status is IDENTITY_VERIFICATION_NEEDED
await tanker.verifyIdentity({ passphrase });
// Before 2.0
// After open()
tanker.unlockCurrentDeviceWithPassword(password);

// After 2.0
// After start(), in case status in IDENTITY_REGISTRATION_NEEDED
Verification verification = new PassphraseVerification(passphrase);
tanker.verifyIdentity(verification);
// Before 2.0
// After openWithUserID
[tanker unlockCurrentDeviceWithPassword:password];

// After 2.0
TKRVerification* verification = [TKRVerification verificationFromPassphrase:passphrase];
// After startWithIdentity, in case status is TKRStatusIdentityRegistrationNeeded
[tanker verifyIdentityWithVerification:verification completionHandler:...];

Replace generateAndRegisterUnlockKey with generateVerificationKey

// Before 2.0
// After open()
const unlockKey = await tanker.generateAndRegisterUnlockKey();
// In the unlockRequired callback
await tanker.unlockCurrentDevice({ unlockKey });

// After 2.0
// After start(), in case status is IDENTITY_REGISTRATION_NEEDED
const verificationKey = await tanker.generateVerificationKey();
await tanker.registerIdentity({ verificationKey });
// After start(), in case status is IDENTITY_VERIFICATION_NEEDED
await tanker.verifyIdentity({ verificationKey });
// Before 2.0
// After open()
String unlockKey = tanker.generateAndRegisterUnlockKey().get();
// In the unlockRequired callback
tanker.unlockCurrentDeviceWithUnlockKey(unlockKey);

// After 2.0
// After start(), in case status is IDENTITY_REGISTRATION_NEEDED
String verificationKey = tanker.generateVerificationKey().get();
// After start(), in case status is IDENTITY_VERIFICATION_NEEDED
Verification verification = new VerificationKeyVerification(verificationKey);
tanker.verifyIdentity(verification);
// Before 2.0
// After openWithUserID
[tanker generateAndRegisterUnlockKey];

// in the unlockRequiredHandler
[tanker unlockCurrentDeviceWithUnlockKey:unlockKey];

// After 2.0
[tanker generateVerificationKeyWithCompletionHandler:...];

TKRVerification* verification = [TKRVerification verificationFromVerificationKey:verificationKey];

// After startWithIdentity, in case status is TKRStatusIdentityRegistrationNeeded
[tanker registerIdentityWithVerification:verification completionHandler:...];
// After startWithIdentity, in case status is TKRStatusIdentityVerificationNeeded
[tanker verifyIdentityWithVerification:verification completionHandler:...];

Replace close with stop

// Before 2.0
await tanker.close();

// After 2.0
await tanker.stop();
// Before 2.0
tanker.close();

// After 2.0
tanker.stop();
// Before 2.0
[tanker close];

// After 2.0
[tanker stopWithCompletionHandler];

Replace registeredUnlockMethods et al with getVerificationMethods

// Before 2.0
const methods = tanker.registeredUnlockMethods();
const hasEmail = tanker.hasRegisteredUnlockMethod('email');
const hasAnyMethod = tanker.hasRegisteredUnlockMethods();

// After 2.0
const methods = await tanker.getVerificationMethods(); // Warning: this is now an asynchronous method
const hasEmail = methods.some(m => m.type === 'email');
const hasAnyMethod; // always true, by design
// Before 2.0
List<TankerUnlockMethodInfo> methods = tanker.registeredUnlockMethods();
boolean hasEmail = tanker.hasRegisteredUnlockMethod(TankerUnlockMethod.EMAIL);
boolean hasAnyMethod = tanker.hasRegisteredUnlockMethods();

// After 2.0
List<VerificationMethod> methods = tanker.getVerificationMethods().get();
boolean hasEmail = false;
for (VerificationMethod method : methods) {
  if (method instanceof EmailVerificationMethod) {
    hasEmail = true;
  }
}
boolean hasAnyMethod; // always true, by design
// Before 2.0
[tanker registeredVerificationMethods];

[tanker hasRegisteredUnlockMethod:TKRUnlockMethodEmail];
[tanker hasRegisteredUnlockMethods];

// After 2.0
[tanker verificationMethodsWithCompletionHandler:...];

Replace second call to registerUnlock with setVerificationMethod

See Updating verification methods for details.

By passphrase

// Before 2.0
await tanker.registerUnlock({ password: newPassword });

// After 2.0
await tanker.setVerificationMethod({ passphrase: newPassphrase });
// Before 2.0
TankerUnlockOptions options = new TankerUnlockOptions();
options.setPassword(newPassword);
tanker.registerUnlock(options);

// After 2.0
Verification verification = new PassphraseVerification(newPassphrase);
tanker.setVerificationMethod(verification);
// Before 2.0
TKRUnlockOptions* options = [TKRUnlockOptions defaultOptions];
options.password = newPassword;
[tanker registerUnlock:options];

// After 2.0
TKRVerification* verification = [TKRVerification verificationFromPassphrase:newPassphrase];
[tanker setVerificationMethod:verification completionHandler:...];

By email

// Before 2.0
await tanker.registerUnlock({ email: newEmail });

// After 2.0
await tanker.setVerificationMethod({ email: newEmail, verificationCode });
// Before 2.0
TankerUnlockOptions options = new TankerUnlockOptions();
options.setEmail(newEmail);
tanker.registerUnlock(options);

// After 2.0
Verification verification = new EmailVerification(newEmail, verificationCode);
tanker.setVerificationMethod(verification);
// Before 2.0
TKRUnlockOptions* options = [TKRUnlockOptions defaultOptions];
options.email = newEmail;
[tanker registerUnlock:options];

// After 2.0
TKRVerification* verification = [TKRVerification verificationFromEmail:newEmail code:verificationCode];
[tanker setVerificationMethod:verification completionHandler:...];

Replace user IDs with identities

// Before 2.0
// Using user ids
await tanker.encrypt(text, { shareWithUsers: [bobId, charlieId] });
await tanker.share([resourceId], { shareWithUsers: [bobId, charlieId] });
await tanker.createGroup([bobId, charlieId]);
await tanker.updateGroupMembers(groupId, { usersToAdd: [bobId, charlieId] });

// After 2.0
// Using public identities
await tanker.encrypt(text, { shareWithUsers: [bobPublicIdentity, charliePublicIdentity] });
await tanker.share([resourceId], { shareWithUsers: [bobPublicIdentity, charliePublicIdentity] });
await tanker.createGroup([bobPublicIdentity, charliePublicIdentity]);
await tanker.updateGroupMembers(groupId, { usersToAdd: [bobPublicIdentity, charliePublicIdentity] });
// Before 2.0
TankerEncryptOptions options = new TankerEncryptOptions();
options.shareWithUsers(new String[]{bobId, charlieId});
tanker.encrypt(data, options);

TankerShareOptions options = new TankerShareOptions();
options.shareWithUsers(new String[]{bobId, charlieId});
tanker.share(new String[]{resourceId}, options);

tanker.createGroup(new String[]{bobId, charlieId});
tanker.updateGroupMembers(groupId, new String[]{bobId, charlieId});

// After 2.0
EncryptOptions options = new EncryptOptions();
options.shareWithUsers(new String[]{bobPublicIdentity, charliePublicIdentity});
tanker.encrypt(data, options);

ShareOptions options = new ShareOptions();
options.shareWithUsers(new String[]{bobPublicIdentity, charliePublicIdentity});
tanker.share(new String[]{resourceId}, options);

tanker.createGroup(new String[]{bobPublicIdentity, charliePublicIdentity});
tanker.updateGroupMembers(groupId, new String[]{bobPublicIdentity, charliePublicIdentity});
// Before 2.0
TKREncryptionOptions* options = [TKREncryptionOptions defaultOptions];
options.shareWithUsers = @[ bobId, charlieId ];
[tanker encryptDataFromString:message options:options];

TKRShareOptions* options = [TKRShareOptions defaultOptions];
options.shareWithUsers = @[ bobId, charlieId ];
[tanker shareResourceIDs:@[ resourceId ] options:options];

[tanker createGroupWithUserIDs:@[ bobId, charlieId ]];
[tanker updateMembersOfGroup:groupId usersToAdd:@[ bobId, charlieId ]];

// After 2.0
TKREncryptionOptions* options = [TKREncryptionOptions defaultOptions];
options.shareWithUsers = @[ bobPublicIdentity, charliePublicIdentity ];
[tanker encryptString:message options:options completionHandler:...];

TKRShareOptions* options = [TKRShareOptions defaultOptions];
options.shareWithUsers = @[ bobPublicIdentity, charliePublicIdentity ];
[tanker shareResourceIDs:@[ resourceId ] options:options completionHandler:...];

[tanker createGroupWithIdentities:@[ bobPublicIdentity, charliePublicIdentity ] completionHandler:...];
[tanker updateMembersOfGroup:groupId usersToAdd:@[ bobPublicIdentity, charliePublicIdentity ] completionHandler:...];