diff --git a/xcode/OLMKit/OLMAccount.m b/xcode/OLMKit/OLMAccount.m
index 58dd4adc07b6340882ef009ae179ee8d165ee013..d56b6b4f1975ad6a5463c15e494f10d364d0c217 100644
--- a/xcode/OLMKit/OLMAccount.m
+++ b/xcode/OLMKit/OLMAccount.m
@@ -44,7 +44,8 @@
         return nil;
     }
     size_t randomLength = olm_create_account_random_length(_account);
-    size_t accountResult = olm_create_account(_account, (void*)[OLMUtility randomBytesOfLength:randomLength].bytes, randomLength);
+    NSMutableData *random = [OLMUtility randomBytesOfLength:randomLength];
+    size_t accountResult = olm_create_account(_account, random.mutableBytes, random.length);
     if (accountResult == olm_error()) {
         const char *error = olm_account_last_error(_account);
         NSLog(@"error creating account: %s", error);
@@ -105,7 +106,8 @@
 
 - (void) generateOneTimeKeys:(NSUInteger)numberOfKeys {
     size_t randomLength = olm_account_generate_one_time_keys_random_length(_account, numberOfKeys);
-    size_t result = olm_account_generate_one_time_keys(_account, numberOfKeys, (void*)[OLMUtility randomBytesOfLength:randomLength].bytes, randomLength);
+    NSMutableData *random = [OLMUtility randomBytesOfLength:randomLength];
+    size_t result = olm_account_generate_one_time_keys(_account, numberOfKeys, random.mutableBytes, random.length);
     if (result == olm_error()) {
         const char *error = olm_account_last_error(_account);
         NSLog(@"error generating keys: %s", error);
diff --git a/xcode/OLMKit/OLMMessage.h b/xcode/OLMKit/OLMMessage.h
index 2b747fb5f964436cb7b1edab1bff879d0dfbab1f..97c748f72efe3878b769ad3c38f0ca812784cf25 100644
--- a/xcode/OLMKit/OLMMessage.h
+++ b/xcode/OLMKit/OLMMessage.h
@@ -8,17 +8,21 @@
 
 #import <Foundation/Foundation.h>
 
-typedef NS_ENUM(NSUInteger, OLMMessageType) {
-    OLMMessageTypeUnknown,
-    OLMMessageTypePreKey,
-    OLMMessageTypeMessage
+/*
+ from olm.hh
+ static const size_t OLM_MESSAGE_TYPE_PRE_KEY = 0;
+ static const size_t OLM_MESSAGE_TYPE_MESSAGE = 1;
+ */
+typedef NS_ENUM(NSInteger, OLMMessageType) {
+    OLMMessageTypePreKey = 0,
+    OLMMessageTypeMessage = 1
 };
 
 @interface OLMMessage : NSObject
 
-@property (nonatomic, readonly, nonnull) NSString *message;
+@property (nonatomic, copy, readonly, nonnull) NSString *ciphertext;
 @property (readonly) OLMMessageType type;
 
-- (nonnull instancetype) initWithMessage:(nonnull NSString*)message type:(OLMMessageType)type;
+- (nullable instancetype) initWithCiphertext:(nonnull NSString*)ciphertext type:(OLMMessageType)type;
 
 @end
diff --git a/xcode/OLMKit/OLMMessage.m b/xcode/OLMKit/OLMMessage.m
index ce732ecc61838571b474be005aca07a4cfd1be99..d0cfb41e2b3f35d08fed211cfd6d579b2f3b8f79 100644
--- a/xcode/OLMKit/OLMMessage.m
+++ b/xcode/OLMKit/OLMMessage.m
@@ -10,4 +10,15 @@
 
 @implementation OLMMessage
 
+- (nullable instancetype) initWithCiphertext:(nonnull NSString*)ciphertext type:(OLMMessageType)type {
+    NSParameterAssert(ciphertext != nil);
+    self = [super init];
+    if (!self) {
+        return nil;
+    }
+    _ciphertext = [ciphertext copy];
+    _type = type;
+    return self;
+}
+
 @end
diff --git a/xcode/OLMKit/OLMSession.h b/xcode/OLMKit/OLMSession.h
index 196900fc60498ad1becac4c4a59073bc1f3df695..1a075e4e769ea4533d4806824bd2e39dbb84d08a 100644
--- a/xcode/OLMKit/OLMSession.h
+++ b/xcode/OLMKit/OLMSession.h
@@ -9,21 +9,30 @@
 #import <Foundation/Foundation.h>
 #import "OLMSerializable.h"
 #import "OLMAccount.h"
+#import "OLMMessage.h"
 
 @interface OLMSession : NSObject <OLMSerializable>
 
-- (instancetype) initOutboundSessionWithAccount:(OLMAccount*)account theirIdentityKey:(NSData*)theirIdentityKey theirOneTimeKey:(NSData*)theirOneTimeKey;
+@property (nonatomic, strong) OLMAccount *account;
 
-- (instancetype) initInboundSessionWithAccount:(OLMAccount*)account oneTimeKeyMessage:(NSData*)oneTimeKeyMessage;
+- (instancetype) initOutboundSessionWithAccount:(OLMAccount*)account theirIdentityKey:(NSString*)theirIdentityKey theirOneTimeKey:(NSString*)theirOneTimeKey;
 
-- (instancetype) initInboundSessionWithAccount:(OLMAccount*)account theirIdentityKey:(NSData*)theirIdentityKey oneTimeKeyMessage:(NSData*)oneTimeKeyMessage;
+- (instancetype) initInboundSessionWithAccount:(OLMAccount*)account oneTimeKeyMessage:(NSString*)oneTimeKeyMessage;
 
-- (NSData*) sessionIdentifier;
+- (instancetype) initInboundSessionWithAccount:(OLMAccount*)account theirIdentityKey:(NSString*)theirIdentityKey oneTimeKeyMessage:(NSString*)oneTimeKeyMessage;
 
-- (BOOL) matchesInboundSession:(NSData*)oneTimeKeyMessage;
+- (NSString*) sessionIdentifier;
 
-- (BOOL) matchesInboundSessionFrom:(NSData*)theirIdentityKey oneTimeKeyMessage:(NSData *)oneTimeKeyMessage;
+- (BOOL) matchesInboundSession:(NSString*)oneTimeKeyMessage;
 
-- (void) removeOneTimeKeys;
+- (BOOL) matchesInboundSessionFrom:(NSString*)theirIdentityKey oneTimeKeyMessage:(NSString *)oneTimeKeyMessage;
+
+- (BOOL) removeOneTimeKeys;
+
+/** UTF-8 plaintext -> base64 ciphertext */
+- (OLMMessage*) encryptMessage:(NSString*)message;
+
+/** base64 ciphertext -> UTF-8 plaintext */
+- (NSString*) decryptMessage:(OLMMessage*)message;
 
 @end
diff --git a/xcode/OLMKit/OLMSession.m b/xcode/OLMKit/OLMSession.m
index 24a8b36455eb2d01f48d4c77f959e2e4af39ce95..fa7cb6281809f311ecb57c5550cdbe1b1ef7f2f7 100644
--- a/xcode/OLMKit/OLMSession.m
+++ b/xcode/OLMKit/OLMSession.m
@@ -7,6 +7,8 @@
 //
 
 #import "OLMSession.h"
+#import "OLMUtility.h"
+#import "OLMAccount_Private.h"
 @import olm;
 
 @interface OLMSession()
@@ -15,16 +17,167 @@
 
 @implementation OLMSession
 
-- (instancetype) initOutboundSessionWithAccount:(OLMAccount*)account theirIdentityKey:(NSData*)theirIdentityKey theirOneTimeKey:(NSData*)theirOneTimeKey {
-    
+- (void) dealloc {
+    olm_clear_session(_session);
+    free(_session);
 }
 
-- (instancetype) initInboundSessionWithAccount:(OLMAccount*)account oneTimeKeyMessage:(NSData*)oneTimeKeyMessage {
-    
+- (BOOL) initializeSessionMemory {
+    size_t size = olm_session_size();
+    _session = malloc(size);
+    NSParameterAssert(_session != nil);
+    if (!_session) {
+        return NO;
+    }
+    _session = olm_session(_session);
+    NSParameterAssert(_session != nil);
+    if (!_session) {
+        return NO;
+    }
+    return YES;
 }
 
-- (instancetype) initInboundSessionWithAccount:(OLMAccount*)account theirIdentityKey:(NSData*)theirIdentityKey oneTimeKeyMessage:(NSData*)oneTimeKeyMessage {
-    
+- (instancetype) initWithAccount:(OLMAccount*)account {
+    self = [super init];
+    if (!self) {
+        return nil;
+    }
+    BOOL success = [self initializeSessionMemory];
+    if (!success) {
+        return nil;
+    }
+    _account = account;
+    return self;
+}
+
+- (instancetype) initOutboundSessionWithAccount:(OLMAccount*)account theirIdentityKey:(NSString*)theirIdentityKey theirOneTimeKey:(NSString*)theirOneTimeKey {
+    self = [self initWithAccount:account];
+    if (!self) {
+        return nil;
+    }
+    NSMutableData *random = [OLMUtility randomBytesOfLength:olm_create_outbound_session_random_length(_session)];
+    NSData *idKey = [theirIdentityKey dataUsingEncoding:NSUTF8StringEncoding];
+    NSData *otKey = [theirOneTimeKey dataUsingEncoding:NSUTF8StringEncoding];
+    size_t result = olm_create_outbound_session(_session, account.account, idKey.bytes, idKey.length, otKey.bytes, otKey.length, random.mutableBytes, random.length);
+    if (result == olm_error()) {
+        const char *error = olm_session_last_error(_session);
+        NSAssert(NO, @"olm_create_outbound_session error: %s", error);
+        return nil;
+    }
+    return self;
+}
+
+- (instancetype) initInboundSessionWithAccount:(OLMAccount*)account oneTimeKeyMessage:(NSString*)oneTimeKeyMessage {
+    self = [self initWithAccount:account];
+    if (!self) {
+        return nil;
+    }
+    BOOL success = [self initializeSessionMemory];
+    if (!success) {
+        return nil;
+    }
+    NSMutableData *otk = [NSMutableData dataWithData:[oneTimeKeyMessage dataUsingEncoding:NSUTF8StringEncoding]];
+    size_t result = olm_create_inbound_session(_session, account.account, otk.mutableBytes, oneTimeKeyMessage.length);
+    if (result == olm_error()) {
+        const char *error = olm_session_last_error(_session);
+        NSAssert(NO, @"olm_create_inbound_session error: %s", error);
+        return nil;
+    }
+    return self;
+}
+
+- (instancetype) initInboundSessionWithAccount:(OLMAccount*)account theirIdentityKey:(NSString*)theirIdentityKey oneTimeKeyMessage:(NSString*)oneTimeKeyMessage {
+    self = [self initWithAccount:account];
+    if (!self) {
+        return nil;
+    }
+    BOOL success = [self initializeSessionMemory];
+    if (!success) {
+        return nil;
+    }
+    NSData *idKey = [theirIdentityKey dataUsingEncoding:NSUTF8StringEncoding];
+    NSMutableData *otk = [NSMutableData dataWithData:[oneTimeKeyMessage dataUsingEncoding:NSUTF8StringEncoding]];
+    size_t result = olm_create_inbound_session_from(_session, account.account, idKey.bytes, idKey.length, otk.mutableBytes, otk.length);
+    if (result == olm_error()) {
+        const char *error = olm_session_last_error(_session);
+        NSAssert(NO, @"olm_create_inbound_session_from error: %s", error);
+        return nil;
+    }
+    return self;
+}
+
+- (NSString*) sessionIdentifier {
+    size_t length = olm_session_id_length(_session);
+    NSMutableData *idData = [NSMutableData dataWithLength:length];
+    if (!idData) {
+        return nil;
+    }
+    size_t result = olm_session_id(_session, idData.mutableBytes, idData.length);
+    if (result == olm_error()) {
+        const char *error = olm_session_last_error(_session);
+        NSAssert(NO, @"olm_session_id error: %s", error);
+        return nil;
+    }
+    NSString *idString = [[NSString alloc] initWithData:idData encoding:NSUTF8StringEncoding];
+    return idString;
+}
+
+- (OLMMessage*) encryptMessage:(NSString*)message {
+    size_t messageType = olm_encrypt_message_type(_session);
+    size_t randomLength = olm_encrypt_random_length(_session);
+    NSMutableData *random = [OLMUtility randomBytesOfLength:randomLength];
+    NSData *plaintextData = [message dataUsingEncoding:NSUTF8StringEncoding];
+    size_t ciphertextLength = olm_encrypt_message_length(_session, plaintextData.length);
+    NSMutableData *ciphertext = [NSMutableData dataWithLength:ciphertextLength];
+    if (!ciphertext) {
+        return nil;
+    }
+    size_t result = olm_encrypt(_session, plaintextData.bytes, plaintextData.length, random.mutableBytes, random.length, ciphertext.mutableBytes, ciphertext.length);
+    if (result == olm_error()) {
+        const char *error = olm_session_last_error(_session);
+        NSAssert(NO, @"olm_encrypt error: %s", error);
+        return nil;
+    }
+    NSString *ciphertextString = [[NSString alloc] initWithData:ciphertext encoding:NSUTF8StringEncoding];
+    OLMMessage *encryptedMessage = [[OLMMessage alloc] initWithCiphertext:ciphertextString type:messageType];
+    return encryptedMessage;
+}
+
+- (BOOL) removeOneTimeKeys {
+    size_t result = olm_remove_one_time_keys(_account.account, _session);
+    if (result == olm_error()) {
+        const char *error = olm_session_last_error(_session);
+        NSAssert(NO, @"olm_remove_one_time_keys error: %s", error);
+        return NO;
+    }
+    return YES;
+}
+
+- (NSString*) decryptMessage:(OLMMessage*)message {
+    NSParameterAssert(message != nil);
+    NSData *messageData = [message.ciphertext dataUsingEncoding:NSUTF8StringEncoding];
+    if (!messageData) {
+        return nil;
+    }
+    NSMutableData *mutMessage = messageData.mutableCopy;
+    size_t maxPlaintextLength = olm_decrypt_max_plaintext_length(_session, message.type, mutMessage.mutableBytes, mutMessage.length);
+    if (maxPlaintextLength == olm_error()) {
+        const char *error = olm_session_last_error(_session);
+        NSAssert(NO, @"olm_decrypt_max_plaintext_length error: %s", error);
+        return nil;
+    }
+    // message buffer is destroyed by olm_decrypt_max_plaintext_length
+    mutMessage = messageData.mutableCopy;
+    NSMutableData *plaintextData = [NSMutableData dataWithLength:maxPlaintextLength];
+    size_t plaintextLength = olm_decrypt(_session, message.type, mutMessage.mutableBytes, mutMessage.length, plaintextData.mutableBytes, plaintextData.length);
+    if (plaintextLength == olm_error()) {
+        const char *error = olm_session_last_error(_session);
+        NSAssert(NO, @"olm_decrypt error: %s", error);
+        return nil;
+    }
+    plaintextData.length = plaintextLength;
+    NSString *plaintext = [[NSString alloc] initWithData:plaintextData encoding:NSUTF8StringEncoding];
+    return plaintext;
 }
 
 @end
diff --git a/xcode/OLMKit/OLMUtility.h b/xcode/OLMKit/OLMUtility.h
index 0de972502854db25e7672873e1059a3d0879d5cd..8acbf4066bcbb039f4b8ff422d862ea0565c0f6d 100644
--- a/xcode/OLMKit/OLMUtility.h
+++ b/xcode/OLMKit/OLMUtility.h
@@ -10,6 +10,6 @@
 
 @interface OLMUtility : NSObject
 
-+ (NSData*) randomBytesOfLength:(NSUInteger)length;
++ (NSMutableData*) randomBytesOfLength:(NSUInteger)length;
 
 @end
diff --git a/xcode/OLMKit/OLMUtility.m b/xcode/OLMKit/OLMUtility.m
index 01489329f6f9ffd64246b9fd37fb8d02750743b7..5dbe644e888d59485d2733fd11218b2c27f6168a 100644
--- a/xcode/OLMKit/OLMUtility.m
+++ b/xcode/OLMKit/OLMUtility.m
@@ -10,19 +10,16 @@
 
 @implementation OLMUtility
 
-+ (NSData*) randomBytesOfLength:(NSUInteger)length {
-    uint8_t *randomBytes = malloc(length * sizeof(uint8_t));
-    NSParameterAssert(randomBytes != NULL);
-    if (!randomBytes) {
++ (NSMutableData*) randomBytesOfLength:(NSUInteger)length {
+    NSMutableData *randomData = [NSMutableData dataWithLength:length];
+    if (!randomData) {
         return nil;
     }
-    int result = SecRandomCopyBytes(kSecRandomDefault, length, randomBytes);
+    int result = SecRandomCopyBytes(kSecRandomDefault, randomData.length, randomData.mutableBytes);
     if (result != 0) {
-        free(randomBytes);
         return nil;
     }
-    NSData *data = [NSData dataWithBytesNoCopy:randomBytes length:length freeWhenDone:YES];
-    return data;
+    return randomData;
 }
 
 @end
diff --git a/xcode/OLMKitTests/OLMKitTests.m b/xcode/OLMKitTests/OLMKitTests.m
index 944d11ca2df9515b901671035cbde255e350c361..70750572365a173f02744639c4779c0fcd41ec13 100644
--- a/xcode/OLMKitTests/OLMKitTests.m
+++ b/xcode/OLMKitTests/OLMKitTests.m
@@ -31,10 +31,27 @@
     OLMAccount *alice = [[OLMAccount alloc] initNewAccount];
     OLMAccount *bob = [[OLMAccount alloc] initNewAccount];
     [bob generateOneTimeKeys:5];
-    NSDictionary *identityKeys = bob.identityKeys;
-    NSDictionary *oneTimeKeys = bob.oneTimeKeys;
-    NSParameterAssert(identityKeys != nil);
-    NSParameterAssert(oneTimeKeys != nil);
+    NSDictionary *bobIdKeys = bob.identityKeys;
+    NSString *bobIdKey = bobIdKeys[@"curve25519"];
+    NSDictionary *bobOneTimeKeys = bob.oneTimeKeys;
+    NSParameterAssert(bobIdKey != nil);
+    NSParameterAssert(bobOneTimeKeys != nil);
+    __block NSString *bobOneTimeKey = nil;
+    NSDictionary *bobOtkCurve25519 = bobOneTimeKeys[@"curve25519"];
+    [bobOtkCurve25519 enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
+        bobOneTimeKey = obj;
+    }];
+    XCTAssert([bobOneTimeKey isKindOfClass:[NSString class]]);
+    
+    OLMSession *aliceSession = [[OLMSession alloc] initOutboundSessionWithAccount:alice theirIdentityKey:bobIdKey theirOneTimeKey:bobOneTimeKey];
+    NSString *message = @"Hello!";
+    OLMMessage *aliceToBobMsg = [aliceSession encryptMessage:message];
+    
+    OLMSession *bobSession = [[OLMSession alloc] initInboundSessionWithAccount:bob oneTimeKeyMessage:aliceToBobMsg.ciphertext];
+    NSString *plaintext = [bobSession decryptMessage:aliceToBobMsg];
+    XCTAssertEqualObjects(message, plaintext);
+    BOOL success = [bobSession removeOneTimeKeys];
+    XCTAssertTrue(success);
 }