From 6b00f5fe46c22b904ecf3f434ac859cb0fd243ca Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 18 Feb 2018 02:42:37 +0100 Subject: [PATCH] Next step in the #2979 experiment --- .../Source/SPMySQLConnection.m | 50 +++++++++++++------ .../Source/SPMySQLConnectionDelegate.h | 3 +- Source/SPDatabaseDocument.m | 22 +++++--- Source/SPDatabaseStructure.m | 6 ++- 4 files changed, 54 insertions(+), 27 deletions(-) diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m index 7738f6657..d96a3b85e 100644 --- a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m @@ -730,22 +730,27 @@ - (MYSQL *)_makeRawMySQLConnectionWithEncoding:(NSString *)encodingName isMaster - (void)_mysqlConnection:(MYSQL *)connection wantsPassword:(void (^)(const char *passwd))inBlock withPlugin:(const char *)pluginName { - // If a password was supplied, use it; otherwise ask the delegate if appropriate. - // - // Note that password has no charset in mysql: If a user password is set to 'ü' on a latin1 connection - // and you later try to connect on an UTF-8 terminal (or vice versa) it will fail. The MySQL (5.5) manual wrongly states that - // MYSQL_SET_CHARSET_NAME has influence over that, but it does not and could not, since the password is hashed by the client - // before transmitting it to the server and the (5.5) client has no charset support, effectively treating password as - // a NUL-terminated byte array. - // There is one exception, though: The "mysql_clear_password" auth plugin sends the password in plaintext and the server side - // MAY choose to do a charset conversion as appropriate before handing it to whatever backend is used. - // Since we don't know which auth plugin server and client will agree upon, we'll do as the manual says... NSString *passwd = nil; - + + // If a password was supplied, use it; otherwise ask the delegate if appropriate. if (password) { passwd = password; - } else if ([delegate respondsToSelector:@selector(keychainPasswordForConnection:)]) { - passwd = [delegate keychainPasswordForConnection:self]; //TODO pass pluginName to client + } + else if ([delegate respondsToSelector:@selector(keychainPasswordForConnection:authPlugin:)]) { + // It's not clear what charset the plugin name is in: + // In the 5.5 libmysqlclient: + // * For the compiled-in plugins this will simply be the byte sequence as it was in the source code + // * The server requests a plugin in the first packet it sends and gives its own charset (mysql->server_language) + // * However client for the most part ignores the plugin name + // * and it completely ignores the server_language + // * When the client sends its reply (in send_client_reply_packet()) it will send the plugin name together with + // the desired charset+collation (but doesn't apply any charset conversion logic to the values) + // In the JDBC client it works like this: + // * The plugin name in the first packet from the server will always be interpreted as "ASCII" + // * The plugin name in the client response will be encoded in the client's initial charset + // TODO We will just use latin1 for now, as it is the safest fallback + NSString *plugin = [NSString stringWithCString:pluginName encoding:NSISOLatin1StringEncoding]; + passwd = [delegate keychainPasswordForConnection:self authPlugin:plugin]; } // shortcut for empty/nil password @@ -754,7 +759,20 @@ - (void)_mysqlConnection:(MYSQL *)connection wantsPassword:(void (^)(const char return; } - NSStringEncoding connectEncodingNS = [SPMySQLConnection stringEncodingForMySQLCharset:connection->options.charset_name]; + // Note (libmysqlclient 5.5): + // mysql_character_set_name() is only initialized after mysql has read the first packet from the server. + // Before that it will always be latin1, regardless of what was set with mysql_options(). + // That does not mean, that client and server have agreed on a charset already, though! + NSStringEncoding connectEncodingNS = [SPMySQLConnection stringEncodingForMySQLCharset:mysql_character_set_name(connection)]; + + // Note that password has no charset in mysql: If a user password is set to 'ü' on a latin1 connection + // and you later try to connect on an UTF-8 terminal (or vice versa) it will fail. The MySQL (5.5) manual wrongly states that + // MYSQL_SET_CHARSET_NAME has influence over that, but it does not and could not, since the password is hashed by the client + // before transmitting it to the server and the (5.5) client has very limited charset support, + // effectively treating password as a NUL-terminated byte array. + // There is one exception, though: The "mysql_clear_password" auth plugin sends the password in plaintext and the server side + // MAY choose to do a charset conversion as appropriate before handing it to whatever backend is used. + // Since we don't know which auth plugin server and client will agree upon, we'll do as the manual says... NSInteger cLength = [passwd lengthOfBytesUsingEncoding:connectEncodingNS]; if(!cLength || cLength == NSIntegerMax) { @@ -1218,7 +1236,7 @@ + (void)_removeThreadVariables:(NSNotification *)aNotification @end -void PasswordCallback(MYSQL *mysql, const char *plugin, void (^with_password)(const char *passwd)) +void PasswordCallback(MYSQL *mysql, const char *plugin, void (^with_password)(const char *)) { assert(mysql && mysql->sp_context); [(SPMySQLConnection *)mysql->sp_context _mysqlConnection:mysql wantsPassword:with_password withPlugin:plugin]; @@ -1234,7 +1252,7 @@ void PasswordCallback(MYSQL *mysql, const char *plugin, void (^with_password)(co errno_t LegacyMemsetS(void *s, rsize_t smax, int c, rsize_t n) { volatile unsigned char * addr = (volatile unsigned char *)s; - while(n--) *addr++ = c; + while(n--) *addr++ = (unsigned char)c; return 0; } diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnectionDelegate.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConnectionDelegate.h index 1e238c124..69a6ebac7 100644 --- a/Frameworks/SPMySQLFramework/Source/SPMySQLConnectionDelegate.h +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnectionDelegate.h @@ -69,8 +69,9 @@ * can be used to look it up and supplied on demand. * * @param connection The connection instance to supply the password for + * @param pluginName The auth plugin libmysqlclients wants to use the password with */ -- (NSString *)keychainPasswordForConnection:(id)connection; +- (NSString *)keychainPasswordForConnection:(id)connection authPlugin:(NSString *)pluginName; /** * Notifies the delegate that no underlying connection is available, diff --git a/Source/SPDatabaseDocument.m b/Source/SPDatabaseDocument.m index 2340b97ee..7b1d51de4 100644 --- a/Source/SPDatabaseDocument.m +++ b/Source/SPDatabaseDocument.m @@ -119,7 +119,7 @@ - (void)_loadTableTask; - (void) closeAndDisconnect; -- (NSString *)keychainPasswordForConnection:(SPMySQLConnection *)connection; +- (NSString *)keychainPassword; - (NSString *)keychainPasswordForSSHConnection:(SPMySQLConnection *)connection; @end @@ -4686,7 +4686,7 @@ - (NSDictionary *) stateIncludingDetails:(NSDictionary *)detailsToReturn [connection setObject:[self database] forKey:@"database"]; if (includePasswords) { - NSString *pw = [self keychainPasswordForConnection:nil]; + NSString *pw = [self keychainPassword]; if (!pw) pw = [connectionController password]; if (pw) [connection setObject:pw forKey:@"password"]; @@ -4885,9 +4885,8 @@ - (BOOL)setState:(NSDictionary *)stateDetails fromFile:(BOOL)spfBased if ([connection objectForKey:@"password"]) [connectionController setPassword:[connection objectForKey:@"password"]]; else { - NSString *pw = [self keychainPasswordForConnection:nil]; - if (pw) - [connectionController setPassword:pw]; + NSString *pw = [self keychainPassword]; + if (pw) [connectionController setPassword:pw]; } // Set the socket details, whether or not the type is a socket @@ -7150,15 +7149,22 @@ - (void)queryGaveError:(NSString *)error connection:(id)connection /** * Invoked when the current connection needs a password from the Keychain. */ -- (NSString *)keychainPasswordForConnection:(SPMySQLConnection *)connection +- (NSString *)keychainPasswordForConnection:(SPMySQLConnection *)connection authPlugin:(NSString *)pluginName { + //TODO check plugin name to see whether we want to fetch it from keychain + return [self keychainPassword]; +} + +- (NSString *)keychainPassword +{ + NSString *kcItemName = [connectionController connectionKeychainItemName]; // If no keychain item is available, return an empty password - if (![connectionController connectionKeychainItemName]) return nil; + if (!kcItemName) return nil; // Otherwise, pull the password from the keychain using the details from this connection SPKeychain *keychain = [[SPKeychain alloc] init]; - NSString *password = [keychain getPasswordForName:[connectionController connectionKeychainItemName] account:[connectionController connectionKeychainItemAccount]]; + NSString *password = [keychain getPasswordForName:kcItemName account:[connectionController connectionKeychainItemAccount]]; [keychain release]; diff --git a/Source/SPDatabaseStructure.m b/Source/SPDatabaseStructure.m index 9289b13b3..1eb4352a2 100644 --- a/Source/SPDatabaseStructure.m +++ b/Source/SPDatabaseStructure.m @@ -258,6 +258,7 @@ - (void)queryDbStructureWithUserInfo:(NSDictionary *)userInfo goto cleanup_thread_and_pool; } +#if 0 // For future usage - currently unused // If the affected item name and type - for example, table type and table name - were supplied, extract it. NSString *affectedItem = nil; @@ -269,6 +270,7 @@ - (void)queryDbStructureWithUserInfo:(NSDictionary *)userInfo else affectedItem = nil; } +#endif // Delete all stored data for the database to be updated, leaving the structure key [queriedStructure removeObjectForKey:db_id]; @@ -444,9 +446,9 @@ - (NSArray *)allStructureKeys /** * Forward keychain password requests to the database object. */ -- (NSString *)keychainPasswordForConnection:(id)connection +- (NSString *)keychainPasswordForConnection:(id)connection authPlugin:(NSString *)pluginName { - return [delegate keychainPasswordForConnection:connection]; + return [delegate keychainPasswordForConnection:connection authPlugin:pluginName]; } #pragma mark -