+ * MySQL Internals Manual / MySQL Client/Server Protocol / Authentication Method / Secure Password Authentication + * https://dev.mysql.com/doc/internals/en/secure-password-authentication.html + *
* * @param password password - * @param seed seed + * @param seed 20-bytes random data from server * @return encrypted password * @throws NoSuchAlgorithmException no such algorithm exception */ - public static byte[] scramble411(final byte[] password, final byte[] seed) throws NoSuchAlgorithmException { - MessageDigest md = MessageDigest.getInstance("SHA-1"); - byte[] pass1 = md.digest(password); - md.reset(); - byte[] pass2 = md.digest(pass1); - md.reset(); - md.update(seed); - byte[] pass3 = md.digest(pass2); - for (int i = 0; i < pass3.length; i++) { - pass3[i] = (byte) (pass3[i] ^ pass1[i]); + public static byte[] encryptWithMySQL41(final byte[] password, final byte[] seed) throws NoSuchAlgorithmException { + MessageDigest messageDigest = MessageDigest.getInstance("SHA-1"); + byte[] passwordSha1 = messageDigest.digest(password); + byte[] concatSeed = concatSeed(messageDigest, seed, messageDigest.digest(passwordSha1)); + return xorPassword(passwordSha1, concatSeed); + } + + private static byte[] concatSeed(final MessageDigest messageDigest, final byte[] seed, final byte[] passwordSha1) { + messageDigest.update(seed); + messageDigest.update(passwordSha1); + return messageDigest.digest(); + } + + private static byte[] xorPassword(final byte[] passwordSha1, final byte[] concatSeed) { + byte[] result = new byte[concatSeed.length]; + for (int i = 0; i < concatSeed.length; i++) { + result[i] = (byte) (concatSeed[i] ^ passwordSha1[i]); } - return pass3; + return result; } } diff --git a/src/main/java/info/avalon566/shardingscaling/sync/mysql/binlog/packet/auth/ClientAuthenticationPacket.java b/src/main/java/info/avalon566/shardingscaling/sync/mysql/binlog/packet/auth/ClientAuthenticationPacket.java index cce7e71596cfd5658ebee4a28f517f9defe47704..d780b2627dc20335bfd47a13d9ba180382f2cdde 100644 --- a/src/main/java/info/avalon566/shardingscaling/sync/mysql/binlog/packet/auth/ClientAuthenticationPacket.java +++ b/src/main/java/info/avalon566/shardingscaling/sync/mysql/binlog/packet/auth/ClientAuthenticationPacket.java @@ -94,7 +94,7 @@ public final class ClientAuthenticationPacket extends AbstractPacket { DataTypesCodec.writeByte((byte) 0x00, result); } else { try { - byte[] encryptedPassword = MySQLPasswordEncryptor.scramble411(getPassword().getBytes(), scrumbleBuff); + byte[] encryptedPassword = MySQLPasswordEncryptor.encryptWithMySQL41(getPassword().getBytes(), scrumbleBuff); DataTypesCodec.writeLengthCodedBinary(encryptedPassword, result); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("can't encrypt password that will be sent to MySQL server.", e); diff --git a/src/test/java/info/avalon566/shardingscaling/sync/mysql/binlog/MySQLPasswordEncryptorTest.java b/src/test/java/info/avalon566/shardingscaling/sync/mysql/binlog/MySQLPasswordEncryptorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0213f908c8b96a570495d90685ab720a03044b80 --- /dev/null +++ b/src/test/java/info/avalon566/shardingscaling/sync/mysql/binlog/MySQLPasswordEncryptorTest.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package info.avalon566.shardingscaling.sync.mysql.binlog; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import java.security.NoSuchAlgorithmException; + +import org.junit.Test; + +public class MySQLPasswordEncryptorTest { + + @Test + public void assertEncryptWithMySQL41() throws NoSuchAlgorithmException { + byte[] passwordBytes = "password".getBytes(); + byte[] seed = getRandomSeed(); + assertThat(MySQLPasswordEncryptor.encryptWithMySQL41(passwordBytes, seed), is(getExpectedPassword())); + } + + private byte[] getRandomSeed() { + byte[] result = new byte[20]; + for (int i = 0; i < result.length; i++) { + result[i] = (byte) i; + } + return result; + } + + private byte[] getExpectedPassword() { + return new byte[] {-110, -31, 48, -32, -22, -29, 54, -40, 54, 118, -119, -16, -96, -25, 121, -64, -75, -103, 73, -44}; + } +}