diff --git a/xcode/OLMKit/OLMInboundGroupSession.h b/xcode/OLMKit/OLMInboundGroupSession.h
new file mode 100644
index 0000000000000000000000000000000000000000..a5074904a16ecd8e15e48bfec63835d2b272c3c2
--- /dev/null
+++ b/xcode/OLMKit/OLMInboundGroupSession.h
@@ -0,0 +1,29 @@
+/*
+ Copyright 2016 OpenMarket Ltd
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "OLMSerializable.h"
+
+@interface OLMInboundGroupSession : NSObject <OLMSerializable, NSSecureCoding>
+
+- (instancetype) initInboundGroupSessionWithSessionKey:(NSString*)sessionKey;
+
+- (NSString*)sessionIdentifier;
+
+/** base64 ciphertext -> UTF-8 plaintext */
+- (NSString*)decryptMessage:(NSString*)message;
+
+@end
diff --git a/xcode/OLMKit/OLMInboundGroupSession.m b/xcode/OLMKit/OLMInboundGroupSession.m
new file mode 100644
index 0000000000000000000000000000000000000000..d95d1abed6ceb1c252ab77b74211effd7c3d291d
--- /dev/null
+++ b/xcode/OLMKit/OLMInboundGroupSession.m
@@ -0,0 +1,198 @@
+/*
+ Copyright 2016 OpenMarket Ltd
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#import "OLMInboundGroupSession.h"
+
+#import "OLMUtility.h"
+#include "olm/olm.h"
+
+@interface OLMInboundGroupSession ()
+{
+    OlmInboundGroupSession *session;
+}
+@end
+
+
+@implementation OLMInboundGroupSession
+
+- (void)dealloc {
+    olm_clear_inbound_group_session(session);
+    free(session);
+}
+
+- (instancetype)init {
+    self = [super init];
+    if (self)
+    {
+        session = malloc(olm_inbound_group_session_size());
+        if (session) {
+            session = olm_inbound_group_session(session);
+        }
+
+        if (!session) {
+            return nil;
+        }
+    }
+    return self;
+}
+
+- (instancetype)initInboundGroupSessionWithSessionKey:(NSString *)sessionKey {
+    self = [self init];
+    if (self) {
+        NSData *sessionKeyData = [sessionKey dataUsingEncoding:NSUTF8StringEncoding];
+        size_t result = olm_init_inbound_group_session(session, sessionKeyData.bytes, sessionKeyData.length);
+        if (result == olm_error())   {
+            const char *error = olm_inbound_group_session_last_error(session);
+            NSAssert(NO, @"olm_init_inbound_group_session error: %s", error);
+            return nil;
+        }
+    }
+    return self;
+}
+
+- (NSString *)sessionIdentifier {
+    size_t length = olm_inbound_group_session_id_length(session);
+    NSMutableData *idData = [NSMutableData dataWithLength:length];
+    if (!idData) {
+        return nil;
+    }
+    size_t result = olm_inbound_group_session_id(session, idData.mutableBytes, idData.length);
+    if (result == olm_error()) {
+        const char *error = olm_inbound_group_session_last_error(session);
+        NSAssert(NO, @"olm_inbound_group_session_id error: %s", error);
+        return nil;
+    }
+    NSString *idString = [[NSString alloc] initWithData:idData encoding:NSUTF8StringEncoding];
+    return idString;
+}
+
+- (NSString *)decryptMessage:(NSString *)message
+{
+    NSParameterAssert(message != nil);
+    NSData *messageData = [message dataUsingEncoding:NSUTF8StringEncoding];
+    if (!messageData) {
+        return nil;
+    }
+    NSMutableData *mutMessage = messageData.mutableCopy;
+    size_t maxPlaintextLength = olm_group_decrypt_max_plaintext_length(session, mutMessage.mutableBytes, mutMessage.length);
+    if (maxPlaintextLength == olm_error()) {
+        const char *error = olm_inbound_group_session_last_error(session);
+        NSAssert(NO, @"olm_group_decrypt_max_plaintext_length error: %s", error);
+        return nil;
+    }
+    // message buffer is destroyed by olm_group_decrypt_max_plaintext_length
+    mutMessage = messageData.mutableCopy;
+    NSMutableData *plaintextData = [NSMutableData dataWithLength:maxPlaintextLength];
+    size_t plaintextLength = olm_group_decrypt(session, mutMessage.mutableBytes, mutMessage.length, plaintextData.mutableBytes, plaintextData.length);
+    if (plaintextLength == olm_error()) {
+        const char *error = olm_inbound_group_session_last_error(session);
+        NSAssert(NO, @"olm_group_decrypt error: %s", error);
+        return nil;
+    }
+    plaintextData.length = plaintextLength;
+    NSString *plaintext = [[NSString alloc] initWithData:plaintextData encoding:NSUTF8StringEncoding];
+    return plaintext;
+}
+
+
+#pragma mark OLMSerializable
+
+/** Initializes from encrypted serialized data. Will throw error if invalid key or invalid base64. */
+- (instancetype) initWithSerializedData:(NSString *)serializedData key:(NSData *)key error:(NSError *__autoreleasing *)error {
+    self = [self init];
+    if (!self) {
+        return nil;
+    }
+    NSParameterAssert(key.length > 0);
+    NSParameterAssert(serializedData.length > 0);
+    if (key.length == 0 || serializedData.length == 0) {
+        if (error) {
+            *error = [NSError errorWithDomain:@"org.matrix.olm" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Bad length."}];
+        }
+        return nil;
+    }
+    NSMutableData *pickle = [serializedData dataUsingEncoding:NSUTF8StringEncoding].mutableCopy;
+    size_t result = olm_unpickle_inbound_group_session(session, key.bytes, key.length, pickle.mutableBytes, pickle.length);
+    if (result == olm_error()) {
+        const char *olm_error = olm_inbound_group_session_last_error(session);
+        NSString *errorString = [NSString stringWithUTF8String:olm_error];
+        if (error && errorString) {
+            *error = [NSError errorWithDomain:@"org.matrix.olm" code:0 userInfo:@{NSLocalizedDescriptionKey: errorString}];
+        }
+        return nil;
+    }
+    return self;
+}
+
+/** Serializes and encrypts object data, outputs base64 blob */
+- (NSString*) serializeDataWithKey:(NSData*)key error:(NSError**)error {
+    NSParameterAssert(key.length > 0);
+    size_t length = olm_pickle_inbound_group_session_length(session);
+    NSMutableData *pickled = [NSMutableData dataWithLength:length];
+    size_t result = olm_pickle_inbound_group_session(session, key.bytes, key.length, pickled.mutableBytes, pickled.length);
+    if (result == olm_error()) {
+        const char *olm_error = olm_inbound_group_session_last_error(session);
+        NSString *errorString = [NSString stringWithUTF8String:olm_error];
+        if (error && errorString) {
+            *error = [NSError errorWithDomain:@"org.matrix.olm" code:0 userInfo:@{NSLocalizedDescriptionKey: errorString}];
+        }
+        return nil;
+    }
+    NSString *pickleString = [[NSString alloc] initWithData:pickled encoding:NSUTF8StringEncoding];
+    return pickleString;
+}
+
+#pragma mark NSSecureCoding
+
++ (BOOL) supportsSecureCoding {
+    return YES;
+}
+
+#pragma mark NSCoding
+
+- (id)initWithCoder:(NSCoder *)decoder {
+    NSString *version = [decoder decodeObjectOfClass:[NSString class] forKey:@"version"];
+
+    NSError *error = nil;
+
+    if ([version isEqualToString:@"1"]) {
+        NSString *pickle = [decoder decodeObjectOfClass:[NSString class] forKey:@"pickle"];
+        NSData *key = [decoder decodeObjectOfClass:[NSData class] forKey:@"key"];
+
+        self = [self initWithSerializedData:pickle key:key error:&error];
+    }
+
+    NSParameterAssert(error == nil);
+    NSParameterAssert(self != nil);
+    if (!self) {
+        return nil;
+    }
+
+    return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)encoder {
+    NSData *key = [OLMUtility randomBytesOfLength:32];
+    NSError *error = nil;
+    NSString *pickle = [self serializeDataWithKey:key error:&error];
+    NSParameterAssert(pickle.length > 0 && error == nil);
+
+    [encoder encodeObject:pickle forKey:@"pickle"];
+    [encoder encodeObject:key forKey:@"key"];
+    [encoder encodeObject:@"1" forKey:@"version"];
+}
+
+@end
diff --git a/xcode/OLMKit/OLMKit.h b/xcode/OLMKit/OLMKit.h
index 65fedc837f09bfaf02e048ab4a90703117c8f4b8..3865e74952c0a4bb638531c8be47c89c8251c303 100644
--- a/xcode/OLMKit/OLMKit.h
+++ b/xcode/OLMKit/OLMKit.h
@@ -17,3 +17,5 @@ NSString *OLMKitVersionString();
 #import <OLMKit/OLMSession.h>
 #import <OLMKit/OLMMessage.h>
 #import <OLMKit/OLMUtility.h>
+#import <OLMKit/OLMInboundGroupSession.h>
+#import <OLMKit/OLMOutboundGroupSession.h>
diff --git a/xcode/OLMKit/OLMOutboundGroupSession.h b/xcode/OLMKit/OLMOutboundGroupSession.h
new file mode 100644
index 0000000000000000000000000000000000000000..e7a8a9127c0e6630e3f0bed3ea8c7211cf312f03
--- /dev/null
+++ b/xcode/OLMKit/OLMOutboundGroupSession.h
@@ -0,0 +1,31 @@
+/*
+ Copyright 2016 OpenMarket Ltd
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "OLMSerializable.h"
+
+@interface OLMOutboundGroupSession : NSObject <OLMSerializable, NSSecureCoding>
+
+- (instancetype) initOutboundGroupSession;
+
+- (NSString*)sessionIdentifier;
+- (NSUInteger)messageIndex;
+- (NSString*)sessionKey;
+
+/** UTF-8 plaintext -> base64 ciphertext */
+- (NSString*)encryptMessage:(NSString*)message;
+
+@end
diff --git a/xcode/OLMKit/OLMOutboundGroupSession.m b/xcode/OLMKit/OLMOutboundGroupSession.m
new file mode 100644
index 0000000000000000000000000000000000000000..c86fa9f0e36d0a17ad2b72eab5866d618b335d33
--- /dev/null
+++ b/xcode/OLMKit/OLMOutboundGroupSession.m
@@ -0,0 +1,205 @@
+/*
+ Copyright 2016 OpenMarket Ltd
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#import "OLMOutboundGroupSession.h"
+
+#import "OLMUtility.h"
+#include "olm/olm.h"
+
+@interface OLMOutboundGroupSession ()
+{
+    OlmOutboundGroupSession *session;
+}
+@end
+
+@implementation OLMOutboundGroupSession
+
+- (void)dealloc {
+    olm_clear_outbound_group_session(session);
+    free(session);
+}
+
+- (instancetype)init {
+    self = [super init];
+    if (self)
+    {
+        session = malloc(olm_outbound_group_session_size());
+        if (session) {
+            session = olm_outbound_group_session(session);
+        }
+
+        if (!session) {
+            return nil;
+        }
+    }
+    return self;
+}
+
+- (instancetype)initOutboundGroupSession {
+    self = [self init];
+    if (self) {
+        NSMutableData *random = [OLMUtility randomBytesOfLength:olm_init_outbound_group_session_random_length(session)];
+
+        size_t result = olm_init_outbound_group_session(session, random.mutableBytes, random.length);
+        if (result == olm_error())   {
+            const char *error = olm_outbound_group_session_last_error(session);
+            NSAssert(NO, @"olm_init_outbound_group_session error: %s", error);
+            return nil;
+        }
+    }
+    return self;
+}
+
+- (NSString *)sessionIdentifier {
+    size_t length = olm_outbound_group_session_id_length(session);
+    NSMutableData *idData = [NSMutableData dataWithLength:length];
+    if (!idData) {
+        return nil;
+    }
+    size_t result = olm_outbound_group_session_id(session, idData.mutableBytes, idData.length);
+    if (result == olm_error()) {
+        const char *error = olm_outbound_group_session_last_error(session);
+        NSAssert(NO, @"olm_outbound_group_session_id error: %s", error);
+        return nil;
+    }
+    NSString *idString = [[NSString alloc] initWithData:idData encoding:NSUTF8StringEncoding];
+    return idString;
+}
+
+- (NSUInteger)messageIndex {
+    return olm_outbound_group_session_message_index(session);
+}
+
+- (NSString *)sessionKey {
+    size_t length = olm_outbound_group_session_key_length(session);
+    NSMutableData *sessionKeyData = [NSMutableData dataWithLength:length];
+    if (!sessionKeyData) {
+        return nil;
+    }
+    size_t result = olm_outbound_group_session_key(session, sessionKeyData.mutableBytes, sessionKeyData.length);
+    if (result == olm_error()) {
+        const char *error = olm_outbound_group_session_last_error(session);
+        NSAssert(NO, @"olm_outbound_group_session_key error: %s", error);
+        return nil;
+    }
+    NSString *sessionKey = [[NSString alloc] initWithData:sessionKeyData encoding:NSUTF8StringEncoding];
+    return sessionKey;
+}
+
+- (NSString *)encryptMessage:(NSString *)message {
+    NSData *plaintextData = [message dataUsingEncoding:NSUTF8StringEncoding];
+    size_t ciphertextLength = olm_group_encrypt_message_length(session, plaintextData.length);
+    NSMutableData *ciphertext = [NSMutableData dataWithLength:ciphertextLength];
+    if (!ciphertext) {
+        return nil;
+    }
+    size_t result = olm_group_encrypt(session, plaintextData.bytes, plaintextData.length, ciphertext.mutableBytes, ciphertext.length);
+    if (result == olm_error()) {
+        const char *error = olm_outbound_group_session_last_error(session);
+        NSAssert(NO, @"olm_group_encrypt error: %s", error);
+        return nil;
+    }
+    return [[NSString alloc] initWithData:ciphertext encoding:NSUTF8StringEncoding];
+}
+
+#pragma mark OLMSerializable
+
+/** Initializes from encrypted serialized data. Will throw error if invalid key or invalid base64. */
+- (instancetype) initWithSerializedData:(NSString *)serializedData key:(NSData *)key error:(NSError *__autoreleasing *)error {
+    self = [self init];
+    if (!self) {
+        return nil;
+    }
+    NSParameterAssert(key.length > 0);
+    NSParameterAssert(serializedData.length > 0);
+    if (key.length == 0 || serializedData.length == 0) {
+        if (error) {
+            *error = [NSError errorWithDomain:@"org.matrix.olm" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Bad length."}];
+        }
+        return nil;
+    }
+    NSMutableData *pickle = [serializedData dataUsingEncoding:NSUTF8StringEncoding].mutableCopy;
+    size_t result = olm_unpickle_outbound_group_session(session, key.bytes, key.length, pickle.mutableBytes, pickle.length);
+    if (result == olm_error()) {
+        const char *olm_error = olm_outbound_group_session_last_error(session);
+        NSString *errorString = [NSString stringWithUTF8String:olm_error];
+        if (error && errorString) {
+            *error = [NSError errorWithDomain:@"org.matrix.olm" code:0 userInfo:@{NSLocalizedDescriptionKey: errorString}];
+        }
+        return nil;
+    }
+    return self;
+}
+
+/** Serializes and encrypts object data, outputs base64 blob */
+- (NSString*) serializeDataWithKey:(NSData*)key error:(NSError**)error {
+    NSParameterAssert(key.length > 0);
+    size_t length = olm_pickle_outbound_group_session_length(session);
+    NSMutableData *pickled = [NSMutableData dataWithLength:length];
+    size_t result = olm_pickle_outbound_group_session(session, key.bytes, key.length, pickled.mutableBytes, pickled.length);
+    if (result == olm_error()) {
+        const char *olm_error = olm_outbound_group_session_last_error(session);
+        NSString *errorString = [NSString stringWithUTF8String:olm_error];
+        if (error && errorString) {
+            *error = [NSError errorWithDomain:@"org.matrix.olm" code:0 userInfo:@{NSLocalizedDescriptionKey: errorString}];
+        }
+        return nil;
+    }
+    NSString *pickleString = [[NSString alloc] initWithData:pickled encoding:NSUTF8StringEncoding];
+    return pickleString;
+}
+
+#pragma mark NSSecureCoding
+
++ (BOOL) supportsSecureCoding {
+    return YES;
+}
+
+#pragma mark NSCoding
+
+- (id)initWithCoder:(NSCoder *)decoder {
+    NSString *version = [decoder decodeObjectOfClass:[NSString class] forKey:@"version"];
+
+    NSError *error = nil;
+
+    if ([version isEqualToString:@"1"]) {
+        NSString *pickle = [decoder decodeObjectOfClass:[NSString class] forKey:@"pickle"];
+        NSData *key = [decoder decodeObjectOfClass:[NSData class] forKey:@"key"];
+
+        self = [self initWithSerializedData:pickle key:key error:&error];
+    }
+
+    NSParameterAssert(error == nil);
+    NSParameterAssert(self != nil);
+    if (!self) {
+        return nil;
+    }
+
+    return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)encoder {
+    NSData *key = [OLMUtility randomBytesOfLength:32];
+    NSError *error = nil;
+    NSString *pickle = [self serializeDataWithKey:key error:&error];
+    NSParameterAssert(pickle.length > 0 && error == nil);
+
+    [encoder encodeObject:pickle forKey:@"pickle"];
+    [encoder encodeObject:key forKey:@"key"];
+    [encoder encodeObject:@"1" forKey:@"version"];
+}
+
+@end
diff --git a/xcode/OLMKitTests/OLMKitGroupTests.m b/xcode/OLMKitTests/OLMKitGroupTests.m
new file mode 100644
index 0000000000000000000000000000000000000000..bcf83ef5c744b5e0b9b5d8ebf81ad056b74551b5
--- /dev/null
+++ b/xcode/OLMKitTests/OLMKitGroupTests.m
@@ -0,0 +1,39 @@
+//
+//  OLMKitGroupTests.m
+//  OLMKit
+//
+//  Created by Emmanuel ROHEE on 10/10/16.
+//  Copyright © 2016 matrix.org. All rights reserved.
+//
+
+#import <XCTest/XCTest.h>
+
+@interface OLMKitGroupTests : XCTestCase
+
+@end
+
+@implementation OLMKitGroupTests
+
+- (void)setUp {
+    [super setUp];
+    // Put setup code here. This method is called before the invocation of each test method in the class.
+}
+
+- (void)tearDown {
+    // Put teardown code here. This method is called after the invocation of each test method in the class.
+    [super tearDown];
+}
+
+- (void)testExample {
+    // This is an example of a functional test case.
+    // Use XCTAssert and related functions to verify your tests produce the correct results.
+}
+
+- (void)testPerformanceExample {
+    // This is an example of a performance test case.
+    [self measureBlock:^{
+        // Put the code you want to measure the time of here.
+    }];
+}
+
+@end