Buscar..
Sintaxis
- kSecClassGenericPassword // Una clave de valor que representa una contraseña que no es de Internet
- kSecClassInternetPassword // Una clave de valor que representa una contraseña de Internet
- kSecClassCertificate // Una clave de valor que representa un certificado
- kSecClassCertificate // Una clave de valor que representa una clave
- kSecClassIdentity // Una clave de valor que representa una identidad, que es un certificado más una clave
Observaciones
iOS almacena información privada como contraseñas, claves de cifrado, certificados e identidades en un área de almacenamiento seguro llamada Llavero. Esta área de almacenamiento es administrada completamente por un co-procesador llamado Secure Enclave, que está integrado dentro del procesador de la aplicación. Debido a que el llavero se encuentra en un espacio aislado en iOS, los elementos del llavero solo pueden ser recuperados por la aplicación que los colocó allí en primer lugar.
En algunos casos, debe activar el uso compartido de llaves en Xcode para evitar errores.
Para interactuar con el llavero, usamos un marco de CA llamado Servicios de llavero. Para obtener más información, consulte la Guía de programación de servicios de llavero de Apple .
Debido a que Keychain Services está por debajo del nivel Foundation
, está restringido al uso de los tipos CoreFoundation
. Como resultado, la mayoría de los objetos se representan internamente como CFDictionary
s con CFString
s como sus claves y una variedad de tipos de CoreFoundation
como sus valores.
Mientras que Keychain Services se incluye como parte del marco de Security
, la importación de Foundation
suele ser una buena opción, ya que incluye algunas funciones de ayuda en el backend.
Además, si no desea tratar directamente con Keychain Services, Apple ofrece el proyecto de muestra Swift de Keychain genérico que proporciona tipos Swift que usan los servicios de Keychain entre bastidores.
Agregando una contraseña al llavero
Cada elemento de llavero se representa con mayor frecuencia como un CFDictionary
. Sin embargo, puede simplemente usar NSDictionary
en Objective-C y aprovechar el puenteo, o en Swift puede usar Dictionary
y convertir explícitamente a CFDictionary
.
Podrías construir una contraseña con el siguiente diccionario:
Rápido
var dict = [String : AnyObject]()
Primero, necesita un par de clave / valor que le permita al llavero saber que esta es una contraseña. Tenga en cuenta que dado que nuestra clave dict es una String
, debemos convertir cualquier CFString
a una String
explícitamente en Swift 3. CFString no se puede usar como la clave de un Diccionario Swift porque no es Hashable.
Rápido
dict[kSecClass as String] = kSecClassGenericPassword
A continuación, nuestra contraseña puede tener una serie de atributos para describirla y ayudarnos a encontrarla más adelante. Aquí hay una lista de atributos para contraseñas genéricas .
Rápido
// The password will only be accessible when the device is unlocked
dict[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlocked
// Label may help you find it later
dict[kSecAttrLabel as String] = "com.me.myapp.myaccountpassword" as CFString
// Username
dict[kSecAttrAccount as String] = "My Name" as CFString
// Service name
dict[kSecAttrService as String] = "MyService" as CFString
Por último, necesitamos nuestros datos privados reales. Asegúrese de no mantener esto en la memoria por mucho tiempo. Esto debe ser CFData
.
Rápido
dict[kSecValueData as String] = "my_password!!".data(using: .utf8) as! CFData
Finalmente, la función de agregar Servicios de llavero quiere saber cómo debe devolver el elemento de llavero recién construido. Dado que no debe conservar los datos mucho tiempo en la memoria, aquí le explicamos cómo solo podría devolver los atributos:
Rápido
dict[kSecReturnAttributes as String] = kCFBooleanTrue
Ahora hemos construido nuestro artículo. Vamos a añadirlo:
Rápido
var result: AnyObject?
let status = withUnsafeMutablePointer(to: &result) {
SecItemAdd(dict as CFDictionary, UnsafeMutablePointer($0))
}
let newAttributes = result as! Dictionary<String, AnyObject>
Esto coloca los nuevos atributos dictados dentro del result
. SecItemAdd
el diccionario que construimos, así como un puntero hacia donde nos gustaría obtener nuestro resultado. La función luego devuelve un OSStatus
indica éxito o un código de error. Los códigos de resultados se describen aquí .
Encontrar una contraseña en el llavero
Para construir una consulta, necesitamos representarla como un CFDictionary
. También puede usar NSDictionary
en Objective-C o Dictionary
en Swift y convertir a CFDictionary
.
Necesitamos una clave de clase:
Rápido
var dict = [String : AnyObject]()
dict[kSecClass as String] = kSecClassGenericPassword
A continuación, podemos especificar atributos para limitar nuestra búsqueda:
Rápido
// Label
dict[kSecAttrLabel as String] = "com.me.myapp.myaccountpassword" as CFString
// Username
dict[kSecAttrAccount as String] = "My Name" as CFString
// Service name
dict[kSecAttrService as String] = "MyService" as CFString
También podemos especificar claves modificadoras de búsqueda especiales descritas aquí .
Finalmente, necesitamos decir cómo nos gustaría que nos devolvieran nuestros datos. A continuación, solicitaremos que solo la contraseña privada sea devuelta como un objeto CFData
:
Rápido
dict[kSecReturnData as String] = kCFBooleanTrue
Ahora, busquemos:
Rápido
var queryResult: AnyObject?
let status = withUnsafeMutablePointer(to: &queryResult) {
SecItemCopyMatching(dict as CFDictionary, UnsafeMutablePointer($0))
}
// Don't keep this in memory for long!!
let password = String(data: queryResult as! Data, encoding: .utf8)!
Aquí, SecItemCopyMatching
un diccionario de consulta y un puntero hacia donde desea que vaya el resultado. Devuelve un OSStatus
con un código de resultado. Aquí están las posibilidades.
Actualizando una contraseña en el llavero
Como de costumbre, primero necesitamos un CFDictionary
para representar el elemento que queremos actualizar. Esto debe contener todos los valores antiguos para el artículo, incluidos los datos privados antiguos. Luego toma un CFDictionary
de cualquier atributo o los datos en sí que le gustaría cambiar.
Así que primero, vamos a construir una clave de clase y una lista de atributos. Estos atributos pueden limitar nuestra búsqueda, pero debe incluir los atributos y los valores antiguos si los va a cambiar.
Rápido
var dict = [String : AnyObject]()
dict[kSecClass as String] = kSecClassGenericPassword
// Label
dict[kSecAttrLabel as String] = "com.me.myapp.myaccountpassword" as CFString
// Username
dict[kSecAttrAccount as String] = "My Name" as CFString
Ahora debemos agregar los datos antiguos:
Rápido
dict[kSecValueData as String] = "my_password!!".data(using: .utf8) as! CFData
Ahora vamos a crear los mismos atributos pero una contraseña diferente:
Rápido
var newDict = [String : AnyObject]()
newDict[kSecClass as String] = kSecClassGenericPassword
// Label
newDict[kSecAttrLabel as String] = "com.me.myapp.myaccountpassword" as CFString
// Username
newDict[kSecAttrAccount as String] = "My Name" as CFString
// New password
newDict[kSecValueData as String] = "new_password!!".data(using: .utf8) as! CFData
Ahora, solo lo pasamos a Keychain Services:
Rápido
let status = SecItemUpdate(dict as CFDictionary, newDict as CFDictionary)
SecItemUpdate
devuelve un código de estado. Los resultados se describen aquí .
Eliminar una contraseña del llavero
Solo necesitamos una cosa para eliminar un elemento del llavero: un CFDictionary
con atributos que describen los elementos que se eliminarán. Cualquier elemento que coincida con el diccionario de consulta se eliminará de forma permanente, por lo que si solo tiene la intención de eliminar un solo elemento, asegúrese de ser específico con su consulta. Como siempre, podemos usar un NSDictionary
en Objective-C o en Swift podemos usar un Dictionary
y luego convertirlo en CFDictionary
.
Un diccionario de consultas, en este contexto, incluye exclusivamente una clave de clase para describir qué es el elemento y los atributos para describir información sobre el elemento. No se permite la inclusión de restricciones de búsqueda como kSecMatchCaseInsensitive
.
Rápido
var dict = [String : AnyObject]()
dict[kSecClass as String] = kSecClassGenericPassword
// Label
dict[kSecAttrLabel as String] = "com.me.myapp.myaccountpassword" as CFString
// Username
dict[kSecAttrAccount as String] = "My Name" as CFString
Y ahora podemos simplemente eliminarlo:
Rápido
let status = SecItemDelete(dict as CFDictionary)
SecItemDelete
devuelve un OSStatus
. Los códigos de resultados se describen aquí .
Llavero Añadir, actualizar, eliminar y buscar operaciones utilizando un archivo.
Llavero.h
#import <Foundation/Foundation.h>
typedef void (^KeychainOperationBlock)(BOOL successfulOperation, NSData *data, OSStatus status);
@interface Keychain : NSObject
-(id) initWithService:(NSString *) service_ withGroup:(NSString*)group_;
-(void)insertKey:(NSString *)key withData:(NSData *)data withCompletion:(KeychainOperationBlock)completionBlock;
-(void)updateKey:(NSString*)key withData:(NSData*) data withCompletion:(KeychainOperationBlock)completionBlock;
-(void)removeDataForKey:(NSString*)key withCompletionBlock:(KeychainOperationBlock)completionBlock;
-(void)findDataForKey:(NSString*)key withCompletionBlock:(KeychainOperationBlock)completionBlock;
@end
Llavero.m
#import "Keychain.h"
#import <Security/Security.h>
@implementation Keychain
{
NSString * keychainService;
NSString * keychainGroup;
}
-(id) initWithService:(NSString *)service withGroup:(NSString*)group
{
self =[super init];
if(self) {
keychainService = [NSString stringWithString:service];
if(group) {
keychainGroup = [NSString stringWithString:group];
}
}
return self;
}
-(void)insertKey:(NSString *)key
withData:(NSData *)data
withCompletion:(KeychainOperationBlock)completionBlock
{
NSMutableDictionary * dict =[self prepareDict:key];
[dict setObject:data forKey:(__bridge id)kSecValueData];
[dict setObject:keychainService forKey:(id)kSecAttrService];
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)dict, NULL);
if(errSecSuccess != status) {
DLog(@"Unable add item with key =%@ error:%d",key,(int)status);
if (completionBlock) {
completionBlock(errSecSuccess == status, nil, status);
}
}
if (status == errSecDuplicateItem) {
[self updateKey:key withData:data withCompletion:^(BOOL successfulOperation, NSData *updateData, OSStatus updateStatus) {
if (completionBlock) {
completionBlock(successfulOperation, updateData, updateStatus);
}
DLog(@"Found duplication item -- updating key with data");
}];
}
}
-(void)findDataForKey:(NSString *)key
withCompletionBlock:(KeychainOperationBlock)completionBlock
{
NSMutableDictionary *dict = [self prepareDict:key];
[dict setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
[dict setObject:keychainService forKey:(id)kSecAttrService];
[dict setObject:(id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
CFTypeRef result = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)dict,&result);
if( status != errSecSuccess) {
DLog(@"Unable to fetch item for key %@ with error:%d",key,(int)status);
if (completionBlock) {
completionBlock(errSecSuccess == status, nil, status);
}
} else {
if (completionBlock) {
completionBlock(errSecSuccess == status, (__bridge NSData *)result, status);
}
}
}
-(void)updateKey:(NSString *)key
withData:(NSData *)data
withCompletion:(KeychainOperationBlock)completionBlock
{
NSMutableDictionary * dictKey =[self prepareDict:key];
NSMutableDictionary * dictUpdate =[[NSMutableDictionary alloc] init];
[dictUpdate setObject:data forKey:(__bridge id)kSecValueData];
[dictUpdate setObject:keychainService forKey:(id)kSecAttrService];
OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)dictKey, (__bridge CFDictionaryRef)dictUpdate);
if( status != errSecSuccess) {
DLog(@"Unable to remove item for key %@ with error:%d",key,(int)status);
}
if (completionBlock) {
completionBlock(errSecSuccess == status, nil, status);
}
}
-(void)removeDataForKey:(NSString *)key
withCompletionBlock:(KeychainOperationBlock)completionBlock {
NSMutableDictionary *dict = [self prepareDict:key];
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)dict);
if( status != errSecSuccess) {
DLog(@"Unable to remove item for key %@ with error:%d",key,(int)status);
}
if (completionBlock) {
completionBlock(errSecSuccess == status, nil, status);
}
}
#pragma mark Internal methods
-(NSMutableDictionary*) prepareDict:(NSString *) key {
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
[dict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
NSData *encodedKey = [key dataUsingEncoding:NSUTF8StringEncoding];
[dict setObject:encodedKey forKey:(__bridge id)kSecAttrGeneric];
[dict setObject:encodedKey forKey:(__bridge id)kSecAttrAccount];
[dict setObject:keychainService forKey:(__bridge id)kSecAttrService];
[dict setObject:(__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible];
//This is for sharing data across apps
if(keychainGroup != nil) {
[dict setObject:keychainGroup forKey:(__bridge id)kSecAttrAccessGroup];
}
return dict;
}
@end
Llavero de control de acceso (TouchID con contraseña de retorno)
Keychain permite guardar elementos con el atributo SecAccessControl especial que permitirá obtener elementos de Keychain solo después de que el usuario sea autenticado con Touch ID (o código de acceso si se permite dicha reserva). Solo se notifica a la aplicación si la autenticación fue exitosa o no, la IU completa es administrada por iOS.
Primero, se debe crear el objeto SecAccessControl:
Rápido
let error: Unmanaged<CFError>?
guard let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, .userPresence, &error) else {
fatalError("Something went wrong")
}
A continuación, agréguelo al diccionario con la clave kSecAttrAccessControl (que se excluye mutuamente con la clave kSecAttrAccessible que ha estado usando en otros ejemplos):
Rápido
var dictionary = [String : Any]()
dictionary[kSecClass as String] = kSecClassGenericPassword
dictionary[kSecAttrLabel as String] = "com.me.myapp.myaccountpassword" as CFString
dictionary[kSecAttrAccount as String] = "My Name" as CFString
dictionary[kSecValueData as String] = "new_password!!".data(using: .utf8) as! CFData
dictionary[kSecAttrAccessControl as String] = accessControl
Y guárdalo como lo has hecho antes:
Rápido
let lastResultCode = SecItemAdd(query as CFDictionary, nil)
Para acceder a los datos almacenados, solo consulte Keychain para obtener una clave. Keychain Services presentará un cuadro de diálogo de autenticación al usuario y devolverá los datos, o nada, dependiendo de si se proporcionó una huella dactilar adecuada o si se hizo coincidir el código de acceso.
Opcionalmente, se puede especificar una cadena de solicitud:
Rápido
var query = [String: Any]()
query[kSecClass as String] = kSecClassGenericPassword
query[kSecReturnData as String] = kCFBooleanTrue
query[kSecAttrAccount as String] = "My Name" as CFString
query[kSecAttrLabel as String] = "com.me.myapp.myaccountpassword" as CFString
query[kSecUseOperationPrompt as String] = "Please put your fingers on that button" as CFString
var queryResult: AnyObject?
let status = withUnsafeMutablePointer(to: &queryResult) {
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
}
Preste atención a que el status
será err
si el usuario rechazó, canceló o falló la autorización.
Rápido
if status == noErr {
let password = String(data: queryResult as! Data, encoding: .utf8)!
print("Password: \(password)")
} else {
print("Authorization not passed")
}