diff --git a/NEWS b/NEWS
index 1c0a752d33b70e027469b7ea6113822934dcea6b..043c913f4389b9297169fdb9270908dc4ea587c2 100644
--- a/NEWS
+++ b/NEWS
@@ -194,6 +194,7 @@ Misc
  * Add Gnome libsecret-based crypto keystore
  * Add KDE Kwallet-based crypto keystore
  * Add a plaintext keystore
+ * Add Keychain based crypto keystore for iOS, Mac OS X and tvOS
 
 Removed modules
  * Atmo video filter
diff --git a/modules/MODULES_LIST b/modules/MODULES_LIST
index d30f8d12dce9afd581767e3b4f2368db0148ca30..2a9ce23f603fa7414755d58553ddf6f837178747 100644
--- a/modules/MODULES_LIST
+++ b/modules/MODULES_LIST
@@ -203,6 +203,7 @@ $Id$
  * kai: OS/2 audio output
  * karaoke: simple karaoke audio filter
  * kate: kate text bitstream decoder
+ * keychain: Keystore for iOS, Mac OS X and tvOS
  * kva: OS/2 video output
  * kwallet: store secrets via KDE Kwallet
  * libass: Subtitle renderers using libass
diff --git a/modules/keystore/Makefile.am b/modules/keystore/Makefile.am
index 8b417a9d95190151ae2ff070943b15ec92a13e97..c4aaead6d9669783835839b8c607770ca1a4fcfd 100644
--- a/modules/keystore/Makefile.am
+++ b/modules/keystore/Makefile.am
@@ -29,6 +29,14 @@ if HAVE_KWALLET
 keystore_LTLIBRARIES += libkwallet_plugin.la
 endif
 
+libkeychain_plugin_la_SOURCES = keystore/keychain.m
+libkeychain_plugin_la_OBJCFLAGS = $(AM_CFLAGS) -fobjc-arc
+libkeychain_plugin_la_LDFLAGS = $(AM_LDFLAGS) -rpath '$(keystoredir)' -Wl,-framework,Foundation -Wl,-framework,Security
+
+if HAVE_DARWIN
+keystore_LTLIBRARIES += libkeychain_plugin.la
+endif
+
 keystore_LTLIBRARIES += \
 	$(LTLIBsecret)
 
diff --git a/modules/keystore/keychain.m b/modules/keystore/keychain.m
new file mode 100644
index 0000000000000000000000000000000000000000..9b9be73e893d60d65ede38e4c7f7b596059f21c2
--- /dev/null
+++ b/modules/keystore/keychain.m
@@ -0,0 +1,476 @@
+/*****************************************************************************
+ * keychain.m: Darwin Keychain keystore module
+ *****************************************************************************
+ * Copyright © 2016 VLC authors, VideoLAN and VideoLabs
+ *
+ * Author: Felix Paul Kühne <fkuehne # videolabs.io>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <vlc_common.h>
+#include <vlc_plugin.h>
+#include <vlc_keystore.h>
+
+#import <Foundation/Foundation.h>
+#import <Security/Security.h>
+
+static int Open(vlc_object_t *);
+
+static const int sync_list[] =
+{ 0, 1, 2 };
+static const char *const sync_list_text[] = {
+    N_("Yes"), N_("No"), N_("Any")
+};
+
+static const int accessibility_list[] =
+{ 0, 1, 2, 3, 4, 5, 6, 7 };
+static const char *const accessibility_list_text[] = {
+    N_("System default"),
+    N_("After first unlock"),
+    N_("After first unlock, on this device only"),
+    N_("Always"),
+    N_("When passcode set, on this device only"),
+    N_("Always, on this device only"),
+    N_("When unlocked"),
+    N_("When unlocked, on this device only")
+};
+
+#define SYNC_ITEMS_TEXT N_("Synchronize stored items")
+#define SYNC_ITEMS_LONGTEXT N_("Synchronizes stored items via iCloud Keychain if enabled in the user domain. Requires iOS 7 / Mac OS X 10.9 / tvOS 9.0 or higher.")
+
+#define ACCESSIBILITY_TYPE_TEXT N_("Accessibility type for all future passwords saved to the Keychain")
+
+#define ACCESS_GROUP_TEXT N_("Keychain access group")
+#define ACCESS_GROUP_LONGTEXT N_("Keychain access group as defined by the app entitlements. Requires iOS 3 / Mac OS X 10.9 / tvOS 9.0 or higher.")
+
+/* VLC can be compiled against older SDKs (like before OS X 10.10)
+ * but newer features should still be available.
+ * Hence, re-define things as needed */
+#ifndef kSecAttrSynchronizable
+#define kSecAttrSynchronizable CFSTR("sync")
+#endif
+
+#ifndef kSecAttrSynchronizableAny
+#define kSecAttrSynchronizableAny CFSTR("syna")
+#endif
+
+#ifndef kSecAttrAccessGroup
+#define kSecAttrAccessGroup CFSTR("agrp")
+#endif
+
+#ifndef kSecAttrAccessibleAfterFirstUnlock
+#define kSecAttrAccessibleAfterFirstUnlock CFSTR("ck")
+#endif
+
+#ifndef kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
+#define kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly CFSTR("cku")
+#endif
+
+#ifndef kSecAttrAccessibleAlways
+#define kSecAttrAccessibleAlways CFSTR("dk")
+#endif
+
+#ifndef kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
+#define kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly CFSTR("akpu")
+#endif
+
+#ifndef kSecAttrAccessibleAlwaysThisDeviceOnly
+#define kSecAttrAccessibleAlwaysThisDeviceOnly CFSTR("dku")
+#endif
+
+#ifndef kSecAttrAccessibleWhenUnlocked
+#define kSecAttrAccessibleWhenUnlocked CFSTR("ak")
+#endif
+
+#ifndef kSecAttrAccessibleWhenUnlockedThisDeviceOnly
+#define kSecAttrAccessibleWhenUnlockedThisDeviceOnly CFSTR("aku")
+#endif
+
+vlc_module_begin()
+    set_shortname(N_("Keychain keystore"))
+    set_description(N_("Keystore for iOS, Mac OS X and tvOS"))
+    set_category(CAT_ADVANCED)
+    set_subcategory(SUBCAT_ADVANCED_MISC)
+    add_integer("keychain-synchronize", 1, SYNC_ITEMS_TEXT, SYNC_ITEMS_LONGTEXT, true)
+    change_integer_list(sync_list, sync_list_text)
+    add_integer("keychain-accessibility-type", 0, ACCESSIBILITY_TYPE_TEXT, ACCESSIBILITY_TYPE_TEXT, true)
+    change_integer_list(accessibility_list, accessibility_list_text)
+    add_string("keychain-access-group", NULL, ACCESS_GROUP_TEXT, ACCESS_GROUP_LONGTEXT, true)
+    set_capability("keystore", 100)
+    set_callbacks(Open, NULL)
+vlc_module_end ()
+
+static NSMutableDictionary * CreateQuery(vlc_keystore *p_keystore)
+{
+    NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:3];
+    [dictionary setObject:(__bridge id)kSecClassInternetPassword forKey:(__bridge id)kSecClass];
+
+    [dictionary setObject:@"VLC-Password-Service" forKey:(__bridge id)kSecAttrService];
+
+    const char * psz_access_group = var_InheritString(p_keystore, "keychain-access-group");
+    if (psz_access_group) {
+        [dictionary setObject:[NSString stringWithUTF8String:psz_access_group] forKey:(__bridge id)kSecAttrAccessGroup];
+    }
+
+    id syncValue;
+    int syncMode = var_InheritInteger(p_keystore, "keychain-synchronize");
+
+    if (syncMode == 2) {
+        syncValue = (__bridge id)kSecAttrSynchronizableAny;
+    } else if (syncMode == 0) {
+        syncValue = @(YES);
+    } else {
+        syncValue = @(NO);
+    }
+
+    [dictionary setObject:syncValue forKey:(__bridge id)(kSecAttrSynchronizable)];
+
+    return dictionary;
+}
+
+static NSString * ErrorForStatus(OSStatus status)
+{
+    NSString *message = nil;
+
+    switch (status) {
+#if TARGET_OS_IPHONE
+        case errSecUnimplemented: {
+            message = @"Query unimplemented";
+            break;
+        }
+        case errSecParam: {
+            message = @"Faulty parameter";
+            break;
+        }
+        case errSecAllocate: {
+            message = @"Allocation failure";
+            break;
+        }
+        case errSecNotAvailable: {
+            message = @"Query not available";
+            break;
+        }
+        case errSecDuplicateItem: {
+            message = @"Duplicated item";
+            break;
+        }
+        case errSecItemNotFound: {
+            message = @"Item not found";
+            break;
+        }
+        case errSecInteractionNotAllowed: {
+            message = @"Interaction not allowed";
+            break;
+        }
+        case errSecDecode: {
+            message = @"Decoding failure";
+            break;
+        }
+        case errSecAuthFailed: {
+            message = @"Authentication failure";
+            break;
+        }
+        case -34018: {
+            message = @"iCloud Keychain failure";
+            break;
+        }
+        default: {
+            message = @"Unknown generic error";
+        }
+#else
+        default:
+            message = (__bridge_transfer NSString *)SecCopyErrorMessageString(status, NULL);
+#endif
+    }
+
+    return message;
+}
+
+static void SetAccessibilityForQuery(vlc_keystore *p_keystore,
+                                     NSMutableDictionary *query)
+{
+    int accessibilityType = var_InheritInteger(p_keystore, "keychain-accessibility-type");
+    switch (accessibilityType) {
+        case 1:
+            [query setObject:(__bridge id)kSecAttrAccessibleAfterFirstUnlock forKey:(__bridge id)kSecAttrAccessible];
+            break;
+        case 2:
+            [query setObject:(__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible];
+            break;
+        case 3:
+            [query setObject:(__bridge id)kSecAttrAccessibleAlways forKey:(__bridge id)kSecAttrAccessible];
+            break;
+        case 4:
+            [query setObject:(__bridge id)kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible];
+            break;
+        case 5:
+            [query setObject:(__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible];
+            break;
+        case 6:
+            [query setObject:(__bridge id)kSecAttrAccessibleWhenUnlocked forKey:(__bridge id)kSecAttrAccessible];
+            break;
+        case 7:
+            [query setObject:(__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible];
+            break;
+        default:
+            break;
+    }
+}
+
+static void SetAttributesForQuery(const char *const ppsz_values[KEY_MAX], NSMutableDictionary *query, const char *psz_label)
+{
+    const char *psz_protocol = ppsz_values[KEY_PROTOCOL];
+    const char *psz_user = ppsz_values[KEY_USER];
+    const char *psz_server = ppsz_values[KEY_SERVER];
+    const char *psz_path = ppsz_values[KEY_PATH];
+    const char *psz_port = ppsz_values[KEY_PORT];
+
+    if (psz_label) {
+        [query setObject:[NSString stringWithUTF8String:psz_label] forKey:(__bridge id)kSecAttrLabel];
+    }
+    if (psz_protocol) {
+        [query setObject:[NSString stringWithUTF8String:psz_protocol] forKey:(__bridge id)kSecAttrProtocol];
+    }
+    if (psz_user) {
+        [query setObject:[NSString stringWithUTF8String:psz_user] forKey:(__bridge id)kSecAttrAccount];
+    }
+    if (psz_server) {
+        [query setObject:[NSString stringWithUTF8String:psz_server] forKey:(__bridge id)kSecAttrServer];
+    }
+    if (psz_path) {
+        [query setObject:[NSString stringWithUTF8String:psz_path] forKey:(__bridge id)kSecAttrPath];
+    }
+    if (psz_port) {
+        [query setObject:[NSString stringWithUTF8String:psz_port] forKey:(__bridge id)kSecAttrPort];
+    }
+}
+
+static int CopyEntryValues(const char * ppsz_dst[KEY_MAX], const char *const ppsz_src[KEY_MAX])
+{
+    for (unsigned int i = 0; i < KEY_MAX; ++i)
+    {
+        if (ppsz_src[i])
+        {
+            ppsz_dst[i] = strdup(ppsz_src[i]);
+            if (!ppsz_dst[i])
+                return VLC_EGENERIC;
+        }
+    }
+    return VLC_SUCCESS;
+}
+
+static int Store(vlc_keystore *p_keystore,
+                 const char *const ppsz_values[KEY_MAX],
+                 const uint8_t *p_secret,
+                 size_t i_secret_len,
+                 const char *psz_label)
+{
+    OSStatus status;
+
+    if (!ppsz_values[KEY_PROTOCOL] || !p_secret) {
+        return VLC_EGENERIC;
+    }
+
+    NSMutableDictionary *query = nil;
+    NSMutableDictionary *searchQuery = CreateQuery(p_keystore);
+
+    /* set attributes */
+    SetAttributesForQuery(ppsz_values, searchQuery, psz_label);
+
+    /* search */
+    status = SecItemCopyMatching((__bridge CFDictionaryRef)searchQuery, nil);
+
+    /* create storage unit */
+    NSData *secretData = [[NSString stringWithFormat:@"%s", p_secret] dataUsingEncoding:NSUTF8StringEncoding];
+
+    if (status == errSecSuccess) {
+        /* item already existed in keychain, let's update */
+        query = [[NSMutableDictionary alloc] init];
+
+        /* just set the secret data */
+        [query setObject:secretData forKey:(__bridge id)kSecValueData];
+
+        status = SecItemUpdate((__bridge CFDictionaryRef)(searchQuery), (__bridge CFDictionaryRef)(query));
+    } else if (status == errSecItemNotFound) {
+        /* item not found, let's create! */
+        query = CreateQuery(p_keystore);
+
+        /* set attributes */
+        SetAttributesForQuery(ppsz_values, query, psz_label);
+
+        /* set accessibility */
+        SetAccessibilityForQuery(p_keystore, query);
+
+        /* set secret data */
+        [query setObject:secretData forKey:(__bridge id)kSecValueData];
+
+        status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
+    }
+    if (status != errSecSuccess) {
+        msg_Err(p_keystore, "Storage failed (%i: '%s')", status, [ErrorForStatus(status) UTF8String]);
+        return VLC_EGENERIC;
+    }
+
+    return VLC_SUCCESS;
+}
+
+static unsigned int Find(vlc_keystore *p_keystore,
+                         const char *const ppsz_values[KEY_MAX],
+                         vlc_keystore_entry **pp_entries)
+{
+    CFTypeRef result = NULL;
+
+    NSMutableDictionary *query = CreateQuery(p_keystore);
+    [query setObject:@(YES) forKey:(__bridge id)kSecReturnRef];
+    [query setObject:(__bridge id)kSecMatchLimitAll forKey:(__bridge id)kSecMatchLimit];
+
+    /* set attributes */
+    SetAttributesForQuery(ppsz_values, query, NULL);
+
+    /* search */
+    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
+
+    if (status != errSecSuccess) {
+        msg_Warn(p_keystore, "lookup failed (%i: '%s')", status, [ErrorForStatus(status) UTF8String]);
+        return 0;
+    }
+
+    NSArray *listOfResults = (__bridge_transfer NSArray *)result;
+
+    NSUInteger count = listOfResults.count;
+
+    vlc_keystore_entry *p_entries = calloc(count,
+                                           sizeof(vlc_keystore_entry));
+    if (!p_entries)
+        return 0;
+
+    for (NSUInteger i = 0; i < count; i++) {
+        vlc_keystore_entry *p_entry = &p_entries[i];
+        if (CopyEntryValues((const char **)p_entry->ppsz_values, (const char *const*)ppsz_values) != VLC_SUCCESS) {
+            vlc_keystore_release_entries(p_entries, 1);
+            return 0;
+        }
+
+        SecKeychainItemRef itemRef = (__bridge SecKeychainItemRef)(listOfResults[i]);
+
+        SecKeychainAttributeInfo attrInfo;
+
+#ifndef NDEBUG
+        attrInfo.count = 1;
+        UInt32 tags[1] = {kSecAccountItemAttr}; //, kSecAccountItemAttr, kSecServerItemAttr, kSecPortItemAttr, kSecProtocolItemAttr, kSecPathItemAttr};
+        attrInfo.tag = tags;
+        attrInfo.format = NULL;
+#endif
+
+        SecKeychainAttributeList *attrList = NULL;
+
+        UInt32 dataLength;
+        void * data;
+
+        status = SecKeychainItemCopyAttributesAndData(itemRef, &attrInfo, NULL, &attrList, &dataLength, &data);
+
+        if (status != noErr) {
+            msg_Err(p_keystore, "Lookup error: %i (%s)", status, [ErrorForStatus(status) UTF8String]);
+            vlc_keystore_release_entries(p_entries, count);
+            return 0;
+        }
+
+#ifndef NDEBUG
+        for (unsigned x = 0; x < attrList->count; x++) {
+            SecKeychainAttribute *attr = &attrList->attr[i];
+            switch (attr->tag) {
+                case kSecLabelItemAttr:
+                    NSLog(@"label %@", [[NSString alloc] initWithBytes:attr->data length:attr->length encoding:NSUTF8StringEncoding]);
+                    break;
+                case kSecAccountItemAttr:
+                    NSLog(@"account %@", [[NSString alloc] initWithBytes:attr->data length:attr->length encoding:NSUTF8StringEncoding]);
+                    break;
+                default:
+                    break;
+            }
+        }
+#endif
+
+        /* we need to do some padding here, as string is expected to be 0 terminated */
+        uint8_t *retData = calloc(1, dataLength + 1);
+        memcpy(retData, data, dataLength);
+
+        vlc_keystore_entry_set_secret(p_entry, retData, dataLength + 1);
+
+        free(retData);
+        SecKeychainItemFreeAttributesAndData(attrList, data);
+    }
+
+    *pp_entries = p_entries;
+
+    return count;
+}
+
+static unsigned int Remove(vlc_keystore *p_keystore,
+                           const char *const ppsz_values[KEY_MAX])
+{
+    OSStatus status;
+
+    NSMutableDictionary *query = CreateQuery(p_keystore);
+
+    SetAttributesForQuery(ppsz_values, query, NULL);
+
+    CFTypeRef result = NULL;
+    [query setObject:@(YES) forKey:(__bridge id)kSecReturnRef];
+    [query setObject:(__bridge id)kSecMatchLimitAll forKey:(__bridge id)kSecMatchLimit];
+
+    BOOL failed = NO;
+    status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
+
+    NSUInteger matchCount = 0;
+
+    if (status == errSecSuccess) {
+        NSArray *matches = (__bridge_transfer NSArray *)result;
+        matchCount = matches.count;
+
+        for (NSUInteger x = 0; x < matchCount; x++) {
+            status = SecKeychainItemDelete((__bridge SecKeychainItemRef _Nonnull)(matches[x]));
+            if (status != noErr) {
+                msg_Err(p_keystore, "Deletion error %i (%s)", status , [ErrorForStatus(status) UTF8String]);
+                failed = YES;
+            }
+        }
+    } else {
+        msg_Err(p_keystore, "Lookup error for deletion %i (%s)", status, [ErrorForStatus(status) UTF8String]);
+        return VLC_EGENERIC;
+    }
+
+    if (failed)
+        return VLC_EGENERIC;
+
+    return matchCount;
+}
+
+static int Open(vlc_object_t *p_this)
+{
+    vlc_keystore *p_keystore = (vlc_keystore *)p_this;
+
+    p_keystore->p_sys = NULL;
+    p_keystore->pf_store = Store;
+    p_keystore->pf_find = Find;
+    p_keystore->pf_remove = Remove;
+
+    return VLC_SUCCESS;
+}
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 13550989053a6d7d05351b00ef506b2458131751..402b954c78e0bfc70ab0a5b5572dc5a30bf0cc50 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -947,6 +947,7 @@ modules/hw/vdpau/chroma.c
 modules/hw/vdpau/deinterlace.c
 modules/hw/vdpau/display.c
 modules/hw/vdpau/sharpen.c
+modules/keystore/keychain.m
 modules/lua/demux.c
 modules/lua/intf.c
 modules/lua/libs/configuration.c
diff --git a/test/modules/keystore/test.c b/test/modules/keystore/test.c
index 4a7aea217146d486b56ebc501e89aeb428093995..caa6fa290594be0e517338a1a6aa7b34439babea 100644
--- a/test/modules/keystore/test.c
+++ b/test/modules/keystore/test.c
@@ -51,7 +51,8 @@ static const struct
     /* Following keystores are tested only when asked explicitly by the tester
      * (with "-a" argv) */
     { "secret", false },
-    { "kwallet", false }
+    { "kwallet", false },
+    { "keychain", false }
 };
 
 static void