From e404594ad5d250f5199fdf7e5d2b6df06738badf Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 24 Jul 2018 13:49:39 +0200 Subject: [PATCH] Complete step 2 of AuthKey generation --- generator/src/main/resources/mtproto_api.tl | 2 +- .../github/lonamiwebs/overgram/Overgram.java | 1 - .../overgram/crypto/Authenticator.java | 74 ++++++++++-- .../overgram/network/MTProtoPlainSender.java | 8 +- .../lonamiwebs/overgram/tl/TLObject.java | 6 + .../overgram/utils/BinaryReader.java | 7 +- .../github/lonamiwebs/overgram/utils/RSA.java | 50 ++++++++ .../lonamiwebs/overgram/utils/Utils.java | 113 ++++++++++++++++++ 8 files changed, 245 insertions(+), 16 deletions(-) create mode 100644 lib/src/main/java/io/github/lonamiwebs/overgram/utils/RSA.java create mode 100644 lib/src/main/java/io/github/lonamiwebs/overgram/utils/Utils.java diff --git a/generator/src/main/resources/mtproto_api.tl b/generator/src/main/resources/mtproto_api.tl index b196c7d..46a5bf5 100644 --- a/generator/src/main/resources/mtproto_api.tl +++ b/generator/src/main/resources/mtproto_api.tl @@ -70,7 +70,7 @@ msg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDeta req_pq_multi#be7e8ef1 nonce:int128 = ResPQ; -req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:bytes q:bytes public_key_fingerprint:long encrypted_data:string = Server_DH_Params; +req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:bytes q:bytes public_key_fingerprint:long encrypted_data:bytes = Server_DH_Params; set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:bytes = Set_client_DH_params_answer; diff --git a/lib/src/main/java/io/github/lonamiwebs/overgram/Overgram.java b/lib/src/main/java/io/github/lonamiwebs/overgram/Overgram.java index e469017..a48ab8a 100644 --- a/lib/src/main/java/io/github/lonamiwebs/overgram/Overgram.java +++ b/lib/src/main/java/io/github/lonamiwebs/overgram/Overgram.java @@ -4,7 +4,6 @@ import io.github.lonamiwebs.overgram.crypto.Authenticator; import io.github.lonamiwebs.overgram.network.MTProtoPlainSender; import io.github.lonamiwebs.overgram.network.connection.Connection; import io.github.lonamiwebs.overgram.network.connection.TcpFull; -import io.github.lonamiwebs.overgram.utils.BinaryWriter; import java.io.IOException; diff --git a/lib/src/main/java/io/github/lonamiwebs/overgram/crypto/Authenticator.java b/lib/src/main/java/io/github/lonamiwebs/overgram/crypto/Authenticator.java index ac550f0..4287e64 100644 --- a/lib/src/main/java/io/github/lonamiwebs/overgram/crypto/Authenticator.java +++ b/lib/src/main/java/io/github/lonamiwebs/overgram/crypto/Authenticator.java @@ -1,22 +1,82 @@ package io.github.lonamiwebs.overgram.crypto; import io.github.lonamiwebs.overgram.network.MTProtoPlainSender; +import io.github.lonamiwebs.overgram.tl.Abstract; import io.github.lonamiwebs.overgram.tl.Functions; -import io.github.lonamiwebs.overgram.tl.TLObject; import io.github.lonamiwebs.overgram.tl.Types; +import io.github.lonamiwebs.overgram.utils.RSA; +import io.github.lonamiwebs.overgram.utils.Utils; +import javafx.util.Pair; import java.io.IOException; import java.math.BigInteger; -import java.security.SecureRandom; public class Authenticator { public static void doAuthentication(final MTProtoPlainSender sender) throws IOException, ClassNotFoundException { - final byte[] nonceBytes = new byte[16]; - new SecureRandom().nextBytes(nonceBytes); - final BigInteger nonce = new BigInteger(nonceBytes); + final BigInteger nonce = new BigInteger(Utils.randomBytes(16)); + final Types.ResPQ resPq = sender.send( + new Functions.ReqPqMulti().nonce(nonce)); - final TLObject result = sender.send(new Functions.ReqPqMulti().nonce(nonce)); - assert result instanceof Types.ResPQ; + if (!resPq.nonce().equals(nonce)) { + throw new SecurityException("Step 1 response nonce invalid"); + } + + // Note that Java uses big endian, which is correct here + final BigInteger pq = new BigInteger(resPq.pq()); + final Pair pqPair = Utils.factorize(pq); + final BigInteger p = pqPair.getKey(); + final BigInteger q = pqPair.getValue(); + + // Once again, as big endian + final byte[] pBytes = p.toByteArray(); + final byte[] qBytes = q.toByteArray(); + + final BigInteger newNonce = new BigInteger(Utils.randomBytes(32)); + final byte[] pqInnerData = new Types.PQInnerData() + .pq(resPq.pq()) + .p(pBytes) + .q(qBytes) + .nonce(resPq.nonce()) + .serverNonce(resPq.serverNonce()) + .newNonce(newNonce) + .serializeToBytes(); + + byte[] cipherText = null; + long targetFingerprint = 0; + for (final long fingerprint : resPq.serverPublicKeyFingerprints()) { + cipherText = RSA.encrypt(fingerprint, pqInnerData); + if (cipherText != null) { + targetFingerprint = fingerprint; + break; + } + } + + if (cipherText == null) { + throw new SecurityException("Step 2 could not find a known RSA key"); + } + + final Abstract.ServerDHParams abstractDhParams = sender.send( + new Functions.ReqDHParams() + .nonce(resPq.nonce()) + .serverNonce(resPq.serverNonce()) + .p(pBytes) + .q(qBytes) + .publicKeyFingerprint(targetFingerprint) + .encryptedData(cipherText) + ); + + if (abstractDhParams instanceof Types.ServerDHParamsFail) { + // We could also check its nonce etc. but not needed + throw new SecurityException("Step 2 generation of DH params failed"); + } + + final Types.ServerDHParamsOk dhParams = (Types.ServerDHParamsOk) abstractDhParams; + if (!dhParams.nonce().equals(resPq.nonce())) { + throw new SecurityException("Step 2 DH params have invalid nonce"); + } + if (!dhParams.serverNonce().equals(resPq.serverNonce())) { + throw new SecurityException("Step 2 DH params have invalid nonce"); + } } } diff --git a/lib/src/main/java/io/github/lonamiwebs/overgram/network/MTProtoPlainSender.java b/lib/src/main/java/io/github/lonamiwebs/overgram/network/MTProtoPlainSender.java index 04646ef..d60c2ee 100644 --- a/lib/src/main/java/io/github/lonamiwebs/overgram/network/MTProtoPlainSender.java +++ b/lib/src/main/java/io/github/lonamiwebs/overgram/network/MTProtoPlainSender.java @@ -8,7 +8,6 @@ import io.github.lonamiwebs.overgram.utils.BinaryWriter; import java.io.IOException; import java.nio.ByteBuffer; -import java.nio.ByteOrder; public class MTProtoPlainSender { private final Connection connection; @@ -19,7 +18,7 @@ public class MTProtoPlainSender { this.state = new MTProtoState(); } - public TLObject send(final TLRequest request) throws IOException, ClassNotFoundException { + public T send(final TLRequest request) throws IOException, ClassNotFoundException { final long msgId = state.getNewMsgId(); final BinaryWriter writer = new BinaryWriter(); @@ -42,7 +41,7 @@ public class MTProtoPlainSender { throw new RuntimeException(); } - final BinaryReader reader = new BinaryReader(ByteBuffer.wrap(body).order(ByteOrder.LITTLE_ENDIAN)); + final BinaryReader reader = new BinaryReader(ByteBuffer.wrap(body)); final long authKeyId = reader.readLong(); assert authKeyId == 0; @@ -52,6 +51,7 @@ public class MTProtoPlainSender { final int length = reader.readInt(); assert length > 0; - return reader.readTl(); + //noinspection unchecked + return (T) reader.readTl(); } } diff --git a/lib/src/main/java/io/github/lonamiwebs/overgram/tl/TLObject.java b/lib/src/main/java/io/github/lonamiwebs/overgram/tl/TLObject.java index 6640ac8..952e9b4 100644 --- a/lib/src/main/java/io/github/lonamiwebs/overgram/tl/TLObject.java +++ b/lib/src/main/java/io/github/lonamiwebs/overgram/tl/TLObject.java @@ -7,4 +7,10 @@ public abstract class TLObject { public abstract void serialize(final BinaryWriter writer); public abstract void deserialize(final BinaryReader reader) throws ClassNotFoundException; + + public byte[] serializeToBytes() { + final BinaryWriter writer = new BinaryWriter(); + serialize(writer); + return writer.toBytes(); + } } diff --git a/lib/src/main/java/io/github/lonamiwebs/overgram/utils/BinaryReader.java b/lib/src/main/java/io/github/lonamiwebs/overgram/utils/BinaryReader.java index 749c064..20ecaf2 100644 --- a/lib/src/main/java/io/github/lonamiwebs/overgram/utils/BinaryReader.java +++ b/lib/src/main/java/io/github/lonamiwebs/overgram/utils/BinaryReader.java @@ -5,6 +5,7 @@ import io.github.lonamiwebs.overgram.tl.Types; import java.math.BigInteger; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -13,7 +14,7 @@ public class BinaryReader { private final ByteBuffer buffer; public BinaryReader(final ByteBuffer buffer) { - this.buffer = buffer; + this.buffer = buffer.order(ByteOrder.LITTLE_ENDIAN); } public int readInt() { @@ -47,13 +48,13 @@ public class BinaryReader { public BigInteger readInt128() { final byte[] bytes = new byte[16]; buffer.get(bytes); - return new BigInteger(bytes); + return new BigInteger(Utils.reversed(bytes)); } public BigInteger readInt256() { final byte[] bytes = new byte[32]; buffer.get(bytes); - return new BigInteger(bytes); + return new BigInteger(Utils.reversed(bytes)); } public double readDouble() { diff --git a/lib/src/main/java/io/github/lonamiwebs/overgram/utils/RSA.java b/lib/src/main/java/io/github/lonamiwebs/overgram/utils/RSA.java new file mode 100644 index 0000000..f22ad07 --- /dev/null +++ b/lib/src/main/java/io/github/lonamiwebs/overgram/utils/RSA.java @@ -0,0 +1,50 @@ +package io.github.lonamiwebs.overgram.utils; + +import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream; + +import java.math.BigInteger; + +public class RSA { + // TODO Load keys from "-----BEGIN RSA PUBLIC KEY-----" properely + + public final BigInteger n; + public final BigInteger e; + + private RSA(final BigInteger n, final BigInteger e) { + this.n = n; + this.e = e; + } + + private static RSA getKey(final long fingerprint) { + // Java can't into switch(long) apparently + if (fingerprint == -7306692244673891685L) { + return new RSA(new BigInteger("25081407810410225030931722734886059247598515157516470397242545867550116598436968553551465554653745201634977779380884774534457386795922003815072071558370597290368737862981871277312823942822144802509055492512145589734772907225259038113414940384446493111736999668652848440655603157665903721517224934142301456312994547591626081517162758808439979745328030376796953660042629868902013177751703385501412640560275067171555763725421377065095231095517201241069856888933358280729674273422117201596511978645878544308102076746465468955910659145532699238576978901011112475698963666091510778777356966351191806495199073754705289253783"), new BigInteger("65537")); + } else if (fingerprint == -5738946642031285640L) { + return new RSA(new BigInteger("22347337644621997830323797217583448833849627595286505527328214795712874535417149457567295215523199212899872122674023936713124024124676488204889357563104452250187725437815819680799441376434162907889288526863223004380906766451781702435861040049293189979755757428366240570457372226323943522935844086838355728767565415115131238950994049041950699006558441163206523696546297006014416576123345545601004508537089192869558480948139679182328810531942418921113328804749485349441503927570568778905918696883174575510385552845625481490900659718413892216221539684717773483326240872061786759868040623935592404144262688161923519030977"), new BigInteger("65537")); + } else if (fingerprint == -4344800451088585951L) { + return new RSA(new BigInteger("24403446649145068056824081744112065346446136066297307473868293895086332508101251964919587745984311372853053253457835208829824428441874946556659953519213382748319518214765985662663680818277989736779506318868003755216402538945900388706898101286548187286716959100102939636333452457308619454821845196109544157601096359148241435922125602449263164512290854366930013825808102403072317738266383237191313714482187326643144603633877219028262697593882410403273959074350849923041765639673335775605842311578109726403165298875058941765362622936097839775380070572921007586266115476975819175319995527916042178582540628652481530373407"), new BigInteger("65537")); + } else if (fingerprint == 8205599988028290019L) { + return new RSA(new BigInteger("24573455207957565047870011785254215390918912369814947541785386299516827003508659346069416840622922416779652050319196701077275060353178142796963682024347858398319926119639265555410256455471016400261630917813337515247954638555325280392998950756512879748873422896798579889820248358636937659872379948616822902110696986481638776226860777480684653756042166610633513404129518040549077551227082262066602286208338952016035637334787564972991208252928951876463555456715923743181359826124083963758009484867346318483872552977652588089928761806897223231500970500186019991032176060579816348322451864584743414550721639495547636008351"), new BigInteger("65537")); + } else { + return null; + } + } + + public static byte[] encrypt(final long fingerprint, final byte[] data) throws SecurityException { + final RSA key = getKey(fingerprint); + if (key == null) { + return null; + } + + final ByteOutputStream toEncrypt = new ByteOutputStream(256); + toEncrypt.write(0); // big endian unsigned big integer needs the sign bit 0 + toEncrypt.write(Utils.sha1digest(data)); // 20 bytes + toEncrypt.write(data); // length + toEncrypt.write(Utils.randomBytes(235 - data.length)); // left random padding + + final BigInteger payload = new BigInteger(toEncrypt.getBytes()); + final BigInteger encrypted = payload.modPow(key.e, key.n); + + return encrypted.toByteArray(); + } +} diff --git a/lib/src/main/java/io/github/lonamiwebs/overgram/utils/Utils.java b/lib/src/main/java/io/github/lonamiwebs/overgram/utils/Utils.java new file mode 100644 index 0000000..fd790eb --- /dev/null +++ b/lib/src/main/java/io/github/lonamiwebs/overgram/utils/Utils.java @@ -0,0 +1,113 @@ +package io.github.lonamiwebs.overgram.utils; + +import javafx.util.Pair; + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +public class Utils { + + private static MessageDigest sha1; + private static SecureRandom random; + + static { + random = new SecureRandom(); + try { + sha1 = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException ignored) { + } + } + + public static BigInteger randomBigInteger(final BigInteger max) { + BigInteger result; + do { + result = new BigInteger(max.bitLength(), random); + } while (result.compareTo(max) >= 0); + return result; + } + + public static byte[] randomBytes(final int size) { + final byte[] result = new byte[size]; + random.nextBytes(result); + return result; + } + + // Note that this reverses IN PLACE and returns the same pointer + public static byte[] reversed(final byte[] bytes) { + for (int i = bytes.length / 2; i-- != 0; ) { + final byte temp = bytes[i]; + bytes[i] = bytes[bytes.length - i - 1]; + bytes[bytes.length - i - 1] = temp; + } + return bytes; + } + + public static Pair factorize(final BigInteger pq) { + final BigInteger two = new BigInteger("2"); + + if (pq.mod(two).equals(BigInteger.ZERO)) { + return new Pair<>(two, pq.divide(two)); + } + + BigInteger y = BigInteger.ONE.add(randomBigInteger(pq.subtract(two))); + BigInteger c = BigInteger.ONE.add(randomBigInteger(pq.subtract(two))); + BigInteger m = BigInteger.ONE.add(randomBigInteger(pq.subtract(two))); + BigInteger g = BigInteger.ONE; + BigInteger r = BigInteger.ONE; + BigInteger q = BigInteger.ONE; + BigInteger x = BigInteger.ZERO; + BigInteger ys = BigInteger.ZERO; + + BigInteger countDown; + + while (g.equals(BigInteger.ONE)) { + x = y; + + countDown = r; + while (!countDown.equals(BigInteger.ZERO)) { + countDown = countDown.subtract(BigInteger.ONE); + y = (y.modPow(two, pq).add(c)).mod(pq); + } + + BigInteger k = BigInteger.ZERO; + while (k.compareTo(r) < 0 && g.equals(BigInteger.ONE)) { + ys = y; + + countDown = m.min(r.subtract(k)); + while (!countDown.equals(BigInteger.ZERO)) { + countDown = countDown.subtract(BigInteger.ONE); + y = y.modPow(two, pq).add(c).mod(pq); + q = q.multiply(x.subtract(y).abs()).mod(pq); + } + + g = q.gcd(pq); + k = k.add(m); + } + + r = r.multiply(two); + } + + if (g.equals(pq)) { + do { + ys = ys.modPow(two, pq).add(c).mod(pq); + g = x.subtract(ys).abs().gcd(pq); + } while (g.compareTo(BigInteger.ONE) <= 0); + } + + x = g; + y = pq.divide(g); + return new Pair<>(x.min(y), x.max(y)); + } + + public static byte[] sha1digest(final byte[] data) throws SecurityException { + if (sha1 == null) { + throw new SecurityException("SHA-1 is required but not available"); + } + + final byte[] result = sha1.digest(data); + sha1.reset(); + return result; + } +}