Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to synchronize keychain items to iCloud #497

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,15 @@ Both `setGenericPassword` and `setInternetCredentials` are limited to strings on

## API

### `setGenericPassword(username, password, [{ accessControl, accessible, accessGroup, service, securityLevel }])`
### `setGenericPassword(username, password, [{ accessControl, accessible, accessGroup, service, securityLevel, synchronized }])`

Will store the username/password combination in the secure storage. Resolves to `{service, storage}` or rejects in case of an error. `storage` - is a name of used internal cipher for saving secret; `service` - name used for storing secret in internal storage (empty string resolved to valid default name).

### `getGenericPassword([{ authenticationPrompt, service, accessControl }])`
### `getGenericPassword([{ authenticationPrompt, service, accessControl, synchronized }])`

Will retrieve the username/password combination from the secure storage. Resolves to `{ username, password, service, storage }` if an entry exists or `false` if it doesn't. It will reject only if an unexpected error is encountered like lacking entitlements or permission.

### `resetGenericPassword([{ service }])`
### `resetGenericPassword([{ service, synchronized }])`

Will remove the username/password combination from the secure storage. Resolves to `true` in case of success.

Expand All @@ -123,19 +123,19 @@ Will retrieve all known service names for which a generic password has been stor

_Note_: on iOS this will actully read the encrypted entries, so it will trigger an authentication UI if you have encrypted any entries with password/biometry.

### `setInternetCredentials(server, username, password, [{ accessControl, accessible, accessGroup, securityLevel }])`
### `setInternetCredentials(server, username, password, [{ accessControl, accessible, accessGroup, securityLevel, synchronized }])`

Will store the server/username/password combination in the secure storage. Resolves to `{ username, password, service, storage }`;

### `hasInternetCredentials(server)`
### `hasInternetCredentials(server, { synchronized })`

Will check if the username/password combination for server is available in the secure storage. Resolves to `true` if an entry exists or `false` if it doesn't.

### `getInternetCredentials(server, [{ authenticationPrompt }])`
### `getInternetCredentials(server, [{ authenticationPrompt, synchronized }])`

Will retrieve the server/username/password combination from the secure storage. Resolves to `{ username, password }` if an entry exists or `false` if it doesn't. It will reject only if an unexpected error is encountered like lacking entitlements or permission.

### `resetInternetCredentials(server)`
### `resetInternetCredentials(server, { synchronized })`

Will remove the server/username/password combination from the secure storage.

Expand Down Expand Up @@ -177,6 +177,7 @@ Get security level that is supported on the current device with the current OS.
| **`service`** | All | Reverse domain name qualifier for the service associated with password. | _App bundle ID_ |
| **`storage`** | Android only | Force specific cipher storage usage during saving the password | Select best available storage |
| **`rules`** | Android only | Force following to a specific security rules | `Keychain.RULES.AUTOMATIC_UPGRADE` |
| **`synchronized`** | iOS only | Synchronize keychain item from and to iCloud | `false` |

##### `authenticationPrompt` Properties

Expand Down
45 changes: 35 additions & 10 deletions RNKeychainManager/RNKeychainManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,14 @@ CFStringRef accessibleValue(NSDictionary *options)
return nil;
}

CFBooleanRef synchronizedValue(NSDictionary *options)
{
if (options && options[@"synchronized"]) {
return kCFBooleanTrue;
}
return kCFBooleanFalse;
}

NSString *authenticationPromptValue(NSDictionary *options)
{
if (options && options[@"authenticationPrompt"] != nil && options[@"authenticationPrompt"][@"title"]) {
Expand Down Expand Up @@ -248,23 +256,28 @@ - (void)insertKeychainEntry:(NSDictionary *)attributes
}
}

- (OSStatus)deletePasswordsForService:(NSString *)service
- (OSStatus)deletePasswordsForOptions:(NSDictionary *)options
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it really make sense to have this as a part of the delete query?

Copy link
Author

@m-ruhl m-ruhl Oct 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The options dictionary?
It would be consistence with the other methods..

The alternative in my opinion would be having the service and the flag as params

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oblador can we clarify this?

Copy link
Sponsor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Joel meant if we need the synchronized flag for the reset functions, because it doesn't really change behaviour - does it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my understanding, with the sychronized flag the entry is deleted from the iCloud keychain, otherwise in the device keychain

{
NSString *service = serviceValue(options);
CFBooleanRef synchronized = synchronizedValue(options);
NSDictionary *query = @{
(__bridge NSString *)kSecClass: (__bridge id)(kSecClassGenericPassword),
(__bridge NSString *)kSecAttrService: service,
(__bridge NSString *)kSecAttrSynchronizable: (__bridge id)synchronized,
(__bridge NSString *)kSecReturnAttributes: (__bridge id)kCFBooleanTrue,
(__bridge NSString *)kSecReturnData: (__bridge id)kCFBooleanFalse
};

return SecItemDelete((__bridge CFDictionaryRef) query);
}

- (OSStatus)deleteCredentialsForServer:(NSString *)server
- (OSStatus)deleteCredentialsForServer:(NSString *)server withOptions:(NSDictionary * __nullable)options
{
CFBooleanRef synchronized = synchronizedValue(options);
NSDictionary *query = @{
(__bridge NSString *)kSecClass: (__bridge id)(kSecClassInternetPassword),
(__bridge NSString *)kSecAttrServer: server,
(__bridge NSString *)kSecAttrSynchronizable: (__bridge id)(synchronized),
(__bridge NSString *)kSecReturnAttributes: (__bridge id)kCFBooleanTrue,
(__bridge NSString *)kSecReturnData: (__bridge id)kCFBooleanFalse
};
Expand Down Expand Up @@ -351,14 +364,16 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server
rejecter:(RCTPromiseRejectBlock)reject)
{
NSString *service = serviceValue(options);
CFBooleanRef synchronized = synchronizedValue(options);
NSDictionary *attributes = attributes = @{
(__bridge NSString *)kSecClass: (__bridge id)(kSecClassGenericPassword),
(__bridge NSString *)kSecAttrService: service,
(__bridge NSString *)kSecAttrAccount: username,
(__bridge NSString *)kSecAttrSynchronizable: (__bridge id)(synchronized),
(__bridge NSString *)kSecValueData: [password dataUsingEncoding:NSUTF8StringEncoding]
};

[self deletePasswordsForService:service];
[self deletePasswordsForOptions:options];

[self insertKeychainEntry:attributes withOptions:options resolver:resolve rejecter:reject];
}
Expand All @@ -369,10 +384,12 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server
{
NSString *service = serviceValue(options);
NSString *authenticationPrompt = authenticationPromptValue(options);
CFBooleanRef synchronized = synchronizedValue(options);

NSDictionary *query = @{
(__bridge NSString *)kSecClass: (__bridge id)(kSecClassGenericPassword),
(__bridge NSString *)kSecAttrService: service,
(__bridge NSString *)kSecAttrSynchronizable: (__bridge id)(options[@"synchronized"] ? kCFBooleanTrue : kCFBooleanFalse),
(__bridge NSString *)kSecReturnAttributes: (__bridge id)kCFBooleanTrue,
(__bridge NSString *)kSecReturnData: (__bridge id)kCFBooleanTrue,
(__bridge NSString *)kSecMatchLimit: (__bridge NSString *)kSecMatchLimitOne,
Expand Down Expand Up @@ -411,9 +428,8 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSString *service = serviceValue(options);

OSStatus osStatus = [self deletePasswordsForService:service];
OSStatus osStatus = [self deletePasswordsForOptions:options];

if (osStatus != noErr && osStatus != errSecItemNotFound) {
NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:osStatus userInfo:nil];
Expand All @@ -426,30 +442,36 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server
RCT_EXPORT_METHOD(setInternetCredentialsForServer:(NSString *)server
withUsername:(NSString*)username
withPassword:(NSString*)password
withOptions:(NSDictionary * __nullable)options
withOptions:(NSDictionary *)options
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
[self deleteCredentialsForServer:server];
[self deleteCredentialsForServer:server withOptions: options];
CFBooleanRef synchronized = synchronizedValue(options);

NSDictionary *attributes = @{
(__bridge NSString *)kSecClass: (__bridge id)(kSecClassInternetPassword),
(__bridge NSString *)kSecAttrServer: server,
(__bridge NSString *)kSecAttrAccount: username,
(__bridge NSString *)kSecValueData: [password dataUsingEncoding:NSUTF8StringEncoding]
(__bridge NSString *)kSecValueData: [password dataUsingEncoding:NSUTF8StringEncoding],
(__bridge NSString *)kSecAttrSynchronizable: (__bridge id)(synchronized),
};

[self insertKeychainEntry:attributes withOptions:options resolver:resolve rejecter:reject];
}

RCT_EXPORT_METHOD(hasInternetCredentialsForServer:(NSString *)server
withOptions:(NSDictionary * __nullable)options
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
CFBooleanRef synchronized = synchronizedValue(options);

NSMutableDictionary *queryParts = [[NSMutableDictionary alloc] init];
queryParts[(__bridge NSString *)kSecClass] = (__bridge id)(kSecClassInternetPassword);
queryParts[(__bridge NSString *)kSecAttrServer] = server;
queryParts[(__bridge NSString *)kSecMatchLimit] = (__bridge NSString *)kSecMatchLimitOne;
queryParts[(__bridge NSString *)kSecAttrSynchronizable] = (__bridge id)(synchronized);

if (@available(iOS 9, *)) {
queryParts[(__bridge NSString *)kSecUseAuthenticationUI] = (__bridge NSString *)kSecUseAuthenticationUIFail;
Expand Down Expand Up @@ -478,13 +500,15 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
CFBooleanRef synchronized = synchronizedValue(options);
NSString *authenticationPrompt = authenticationPromptValue(options);
NSDictionary *query = @{
(__bridge NSString *)kSecClass: (__bridge id)(kSecClassInternetPassword),
(__bridge NSString *)kSecAttrServer: server,
(__bridge NSString *)kSecReturnAttributes: (__bridge id)kCFBooleanTrue,
(__bridge NSString *)kSecAttrSynchronizable: (__bridge id)(synchronized),
(__bridge NSString *)kSecReturnData: (__bridge id)kCFBooleanTrue,
(__bridge NSString *)kSecMatchLimit: (__bridge NSString *)kSecMatchLimitOne
(__bridge NSString *)kSecMatchLimit: (__bridge NSString *)kSecMatchLimitOne,
(__bridge NSString *)kSecUseOperationPrompt: authenticationPrompt
};

Expand Down Expand Up @@ -518,10 +542,11 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server
}

RCT_EXPORT_METHOD(resetInternetCredentialsForServer:(NSString *)server
withOptions:(NSDictionary * __nullable)options
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
OSStatus osStatus = [self deleteCredentialsForServer:server];
OSStatus osStatus = [self deleteCredentialsForServer:server withOptions:options];

if (osStatus != noErr && osStatus != errSecItemNotFound) {
NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:osStatus userInfo:nil];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ public void getInternetCredentialsForServer(@NonNull final String server,

@ReactMethod
public void resetInternetCredentialsForServer(@NonNull final String server,
@Nullable final ReadableMap options,
@NonNull final Promise promise) {
resetGenericPassword(server, promise);
}
Expand Down
17 changes: 13 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ type BaseOptions = {|
securityLevel?: SecMinimumLevel,
storage?: SecStorageType,
rules?: SecSecurityRules,
synchronized?: boolean,
|};

type NormalizedOptions = {
Expand Down Expand Up @@ -220,12 +221,14 @@ export async function getAllGenericPasswordServices(): Promise<string[]> {
/**
* Checks if we have a login combination for `server`.
* @param {string} server URL to server.
* @param {object} options An Keychain options object.
* @return {Promise} Resolves to `{service, storage}` when successful
*/
export function hasInternetCredentials(
server: string
server: string,
options?: Options
): Promise<false | Result> {
return RNKeychainManager.hasInternetCredentialsForServer(server);
return RNKeychainManager.hasInternetCredentialsForServer(server, normalizeOptions(options));
}

/**
Expand Down Expand Up @@ -272,8 +275,14 @@ export function getInternetCredentials(
* @param {object} options Keychain options, iOS only
* @return {Promise} Resolves to `true` when successful
*/
export function resetInternetCredentials(server: string): Promise<void> {
return RNKeychainManager.resetInternetCredentialsForServer(server);
export function resetInternetCredentials(
server: string,
options?: Options
): Promise<void> {
return RNKeychainManager.resetInternetCredentialsForServer(
server,
normalizeOptions(options)
);
}

/**
Expand Down
3 changes: 2 additions & 1 deletion typings/react-native-keychain.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ declare module 'react-native-keychain' {
securityLevel?: SECURITY_LEVEL;
storage?: STORAGE_TYPE;
rules?: SECURITY_RULES;
synchronized?: boolean;
}

function setGenericPassword(
Expand All @@ -97,7 +98,7 @@ declare module 'react-native-keychain' {

function getAllGenericPasswordServices(): Promise<string[]>;

function hasInternetCredentials(server: string): Promise<false | Result>;
function hasInternetCredentials(server: string, options?: Options): Promise<false | Result>;

function setInternetCredentials(
server: string,
Expand Down