From 09bb1f9872fb7e294fecedaf9ebec8763c3faead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A1n=20Garc=C3=ADa?= Date: Tue, 13 May 2025 15:24:53 -0300 Subject: [PATCH 1/3] Fix Auth with device & device confirmation --- .../CognitoUserAuthentication.cs | 6 +- .../Util/AuthenticationHelper.cs | 260 +++++++++--------- 2 files changed, 132 insertions(+), 134 deletions(-) diff --git a/src/Amazon.Extensions.CognitoAuthentication/CognitoUserAuthentication.cs b/src/Amazon.Extensions.CognitoAuthentication/CognitoUserAuthentication.cs index 1b6bf24..824980b 100644 --- a/src/Amazon.Extensions.CognitoAuthentication/CognitoUserAuthentication.cs +++ b/src/Amazon.Extensions.CognitoAuthentication/CognitoUserAuthentication.cs @@ -193,7 +193,7 @@ private RespondToAuthChallengeRequest CreateDevicePasswordVerifierAuthRequest(Re string timeStr = DateTime.UtcNow.ToString("ddd MMM d HH:mm:ss \"UTC\" yyyy", CultureInfo.InvariantCulture); - var claimBytes = AuthenticationHelper.AuthenticateDevice(username, deviceKey, devicePassword, deviceKeyGroup, salt, + var claimBytes = AuthenticationHelper.AuthenticateDevice(deviceKey, devicePassword, deviceKeyGroup, salt, challenge.ChallengeParameters[CognitoConstants.ChlgParamSrpB], secretBlock, timeStr, tupleAa); @@ -321,9 +321,9 @@ public virtual async Task RespondToCustomAuthAsync(RespondToCu /// The DeviceKey for the associated CognitoDevice /// The random password for the associated CognitoDevice /// - public DeviceSecretVerifierConfigType GenerateDeviceVerifier(string deviceGroupKey, string devicePass, string username) + public DeviceSecretVerifierConfigType GenerateDeviceVerifier(string deviceGroupKey, string devicePass, string deviceKey) { - return AuthenticationHelper.GenerateDeviceVerifier(deviceGroupKey, devicePass, username); + return AuthenticationHelper.GenerateDeviceVerifier(deviceGroupKey, devicePass, deviceKey); } /// diff --git a/src/Amazon.Extensions.CognitoAuthentication/Util/AuthenticationHelper.cs b/src/Amazon.Extensions.CognitoAuthentication/Util/AuthenticationHelper.cs index 412d074..725c82d 100644 --- a/src/Amazon.Extensions.CognitoAuthentication/Util/AuthenticationHelper.cs +++ b/src/Amazon.Extensions.CognitoAuthentication/Util/AuthenticationHelper.cs @@ -12,10 +12,10 @@ * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ - + using System; -using System.Globalization; -using System.Linq; +using System.Globalization; +using System.Linq; using System.Numerics; using System.Security.Cryptography; using System.Text; @@ -157,136 +157,134 @@ public static byte[] GetPasswordAuthenticationKey( return hkdfSha256.Expand(Encoding.UTF8.GetBytes(DerivedKeyInfo), DerivedKeySizeBytes); } - /// - /// Generates a DeviceSecretVerifierConfigType object based on a CognitoDevice's Key, Group Key, and Password - /// - /// The Group Key of the CognitoDevice - /// A random password for the CognitoDevice (used in the future for logging in via this device) - /// The username of the CognitoDevice user + /// + /// Generates a DeviceSecretVerifierConfigType object based on a CognitoDevice's Key, Group Key, and Password + /// + /// The Group Key of the CognitoDevice + /// A random password for the CognitoDevice (used in the future for logging in via this device) + /// The DeviceKey for the associated CognitoDevice /// - public static DeviceSecretVerifierConfigType GenerateDeviceVerifier(string deviceGroupKey, string devicePass, string username) - { - Random r = new Random(); - byte[] userIdContent = CognitoAuthHelper.CombineBytes( - Encoding.UTF8.GetBytes(deviceGroupKey), - Encoding.UTF8.GetBytes(username), - Encoding.UTF8.GetBytes(":"), + public static DeviceSecretVerifierConfigType GenerateDeviceVerifier(string deviceGroupKey, string devicePass, string deviceKey) + { + Random r = new Random(); + byte[] userIdContent = CognitoAuthHelper.CombineBytes( + Encoding.UTF8.GetBytes(deviceGroupKey), + Encoding.UTF8.GetBytes(deviceKey), + Encoding.UTF8.GetBytes(":"), Encoding.UTF8.GetBytes(devicePass) - ); - - byte[] userIdHash = CognitoAuthHelper.Sha256.ComputeHash(userIdContent); - - byte[] saltBytes = new byte[16]; - RandomNumberGenerator.Create().GetBytes(saltBytes); - // setting the initial byte to 0-127 to avoid negative salt or password verifier error - saltBytes[0] = (byte) r.Next(sbyte.MaxValue); - - byte[] xBytes = CognitoAuthHelper.CombineBytes(saltBytes, userIdHash); - byte[] xDigest = CognitoAuthHelper.Sha256.ComputeHash(xBytes); - BigInteger x = BigIntegerExtensions.FromUnsignedBigEndian(xDigest); - - var v = BigInteger.ModPow(g, x, N); - byte[] vBytes = v.ToBigEndianByteArray(); - - return new DeviceSecretVerifierConfigType - { - PasswordVerifier = Convert.ToBase64String(vBytes), - Salt = Convert.ToBase64String(saltBytes) - }; - } - - /// - /// Generates the claim for authenticating a device through the SRP protocol - /// - /// Username of Cognito User - /// Key of CognitoDevice - /// Password of CognitoDevice - /// GroupKey of CognitoDevice - /// salt provided in ChallengeParameters from Cognito - /// srpb provided in ChallengeParameters from Cognito - /// secret block provided in ChallengeParameters from Cognito - /// En-US Culture of Current Time - /// TupleAa from CreateAaTuple - /// Returns the claim for authenticating the given user - public static byte[] AuthenticateDevice( - string username, - string deviceKey, - string devicePassword, - string deviceGroupKey, - string saltString, - string srpbString, - string secretBlockBase64, - string formattedTimestamp, - Tuple tupleAa) - - { - var B = BigIntegerExtensions.FromUnsignedLittleEndianHex(srpbString); - if (B.TrueMod(N).Equals(BigInteger.Zero)) throw new ArgumentException("B mod N cannot be zero.", nameof(srpbString)); - - var salt = BigIntegerExtensions.FromUnsignedLittleEndianHex(saltString); - var secretBlockBytes = Convert.FromBase64String(secretBlockBase64); - // Need to generate the key to hash the response based on our A and what AWS sent back - var key = GetDeviceAuthenticationKey(username, devicePassword, deviceGroupKey, tupleAa, B, salt); - - // HMAC our data with key (HKDF(S)) (the shared secret) - var msg = CognitoAuthHelper.CombineBytes( - Encoding.UTF8.GetBytes(deviceGroupKey), - Encoding.UTF8.GetBytes(deviceKey), - secretBlockBytes, - Encoding.UTF8.GetBytes(formattedTimestamp) - ); - - using (var hashAlgorithm = new HMACSHA256(key)) - { - return hashAlgorithm.ComputeHash(msg); - } - } - - /// - /// Creates the Device Password Authentication Key based on the SRP protocol - /// - /// Username of Cognito User - /// Password of CognitoDevice - /// GroupKey of CognitoDevice - /// Returned from TupleAa - /// BigInteger SRPB from AWS ChallengeParameters - /// BigInteger salt from AWS ChallengeParameters - /// Returns the password authentication key for the SRP protocol - public static byte[] GetDeviceAuthenticationKey( - string username, - string devicePass, - string deviceGroup, - Tuple Aa, - BigInteger B, - BigInteger salt) - { - // Authenticate the password - // u = H(A, B) - byte[] contentBytes = CognitoAuthHelper.CombineBytes(Aa.Item1.ToBigEndianByteArray(), B.ToBigEndianByteArray()); - byte[] digest = CognitoAuthHelper.Sha256.ComputeHash(contentBytes); - - BigInteger u = BigIntegerExtensions.FromUnsignedBigEndian(digest); - if (u.Equals(BigInteger.Zero)) - { - throw new ArgumentException("Hash of A and B cannot be zero."); - } - - // x = H(salt | H(deviceGroupKey | deviceKey | ":" | devicePassword)) - byte[] deviceContent = CognitoAuthHelper.CombineBytes(Encoding.UTF8.GetBytes(deviceGroup), Encoding.UTF8.GetBytes(username), - Encoding.UTF8.GetBytes(":"), Encoding.UTF8.GetBytes(devicePass)); - byte[] deviceHash = CognitoAuthHelper.Sha256.ComputeHash(deviceContent); - byte[] xBytes = CognitoAuthHelper.CombineBytes(salt.ToBigEndianByteArray(), deviceHash); - - byte[] xDigest = CognitoAuthHelper.Sha256.ComputeHash(xBytes); - BigInteger x = BigIntegerExtensions.FromUnsignedBigEndian(xDigest); - - var gX = BigInteger.ModPow(g, x, N); - // Use HKDF to get final password authentication key - var intValue2 = (B - k * gX).TrueMod(N); - var s_value = BigInteger.ModPow(intValue2, Aa.Item2 + u * x, N); - - HkdfSha256 hkdfSha256 = new HkdfSha256(u.ToBigEndianByteArray(), s_value.ToBigEndianByteArray()); - return hkdfSha256.Expand(Encoding.UTF8.GetBytes(DerivedKeyInfo), DerivedKeySizeBytes); + ); + + byte[] userIdHash = CognitoAuthHelper.Sha256.ComputeHash(userIdContent); + + byte[] saltBytes = new byte[16]; + RandomNumberGenerator.Create().GetBytes(saltBytes); + // setting the initial byte to 0-127 to avoid negative salt or password verifier error + saltBytes[0] = (byte) r.Next(sbyte.MaxValue); + + byte[] xBytes = CognitoAuthHelper.CombineBytes(saltBytes, userIdHash); + byte[] xDigest = CognitoAuthHelper.Sha256.ComputeHash(xBytes); + BigInteger x = BigIntegerExtensions.FromUnsignedBigEndian(xDigest); + + var v = BigInteger.ModPow(g, x, N); + byte[] vBytes = v.ToBigEndianByteArray(); + + return new DeviceSecretVerifierConfigType + { + PasswordVerifier = Convert.ToBase64String(vBytes), + Salt = Convert.ToBase64String(saltBytes) + }; + } + + /// + /// Generates the claim for authenticating a device through the SRP protocol + /// + /// Key of CognitoDevice + /// Password of CognitoDevice + /// GroupKey of CognitoDevice + /// salt provided in ChallengeParameters from Cognito + /// srpb provided in ChallengeParameters from Cognito + /// secret block provided in ChallengeParameters from Cognito + /// En-US Culture of Current Time + /// TupleAa from CreateAaTuple + /// Returns the claim for authenticating the given user + public static byte[] AuthenticateDevice( + string deviceKey, + string devicePassword, + string deviceGroupKey, + string saltString, + string srpbString, + string secretBlockBase64, + string formattedTimestamp, + Tuple tupleAa) + + { + var B = BigIntegerExtensions.FromUnsignedLittleEndianHex(srpbString); + if (B.TrueMod(N).Equals(BigInteger.Zero)) throw new ArgumentException("B mod N cannot be zero.", nameof(srpbString)); + + var salt = BigIntegerExtensions.FromUnsignedLittleEndianHex(saltString); + var secretBlockBytes = Convert.FromBase64String(secretBlockBase64); + // Need to generate the key to hash the response based on our A and what AWS sent back + var key = GetDeviceAuthenticationKey(deviceKey, devicePassword, deviceGroupKey, tupleAa, B, salt); + + // HMAC our data with key (HKDF(S)) (the shared secret) + var msg = CognitoAuthHelper.CombineBytes( + Encoding.UTF8.GetBytes(deviceGroupKey), + Encoding.UTF8.GetBytes(deviceKey), + secretBlockBytes, + Encoding.UTF8.GetBytes(formattedTimestamp) + ); + + using (var hashAlgorithm = new HMACSHA256(key)) + { + return hashAlgorithm.ComputeHash(msg); + } + } + + /// + /// Creates the Device Password Authentication Key based on the SRP protocol + /// + /// The DeviceKey for the associated CognitoDevice + /// Password of CognitoDevice + /// GroupKey of CognitoDevice + /// Returned from TupleAa + /// BigInteger SRPB from AWS ChallengeParameters + /// BigInteger salt from AWS ChallengeParameters + /// Returns the password authentication key for the SRP protocol + public static byte[] GetDeviceAuthenticationKey( + string deviceKey, + string devicePass, + string deviceGroup, + Tuple Aa, + BigInteger B, + BigInteger salt) + { + // Authenticate the password + // u = H(A, B) + byte[] contentBytes = CognitoAuthHelper.CombineBytes(Aa.Item1.ToBigEndianByteArray(), B.ToBigEndianByteArray()); + byte[] digest = CognitoAuthHelper.Sha256.ComputeHash(contentBytes); + + BigInteger u = BigIntegerExtensions.FromUnsignedBigEndian(digest); + if (u.Equals(BigInteger.Zero)) + { + throw new ArgumentException("Hash of A and B cannot be zero."); + } + + // x = H(salt | H(deviceGroupKey | deviceKey | ":" | devicePassword)) + byte[] deviceContent = CognitoAuthHelper.CombineBytes(Encoding.UTF8.GetBytes(deviceGroup), Encoding.UTF8.GetBytes(deviceKey), + Encoding.UTF8.GetBytes(":"), Encoding.UTF8.GetBytes(devicePass)); + byte[] deviceHash = CognitoAuthHelper.Sha256.ComputeHash(deviceContent); + byte[] xBytes = CognitoAuthHelper.CombineBytes(salt.ToBigEndianByteArray(), deviceHash); + + byte[] xDigest = CognitoAuthHelper.Sha256.ComputeHash(xBytes); + BigInteger x = BigIntegerExtensions.FromUnsignedBigEndian(xDigest); + + var gX = BigInteger.ModPow(g, x, N); + // Use HKDF to get final password authentication key + var intValue2 = (B - k * gX).TrueMod(N); + var s_value = BigInteger.ModPow(intValue2, Aa.Item2 + u * x, N); + + HkdfSha256 hkdfSha256 = new HkdfSha256(u.ToBigEndianByteArray(), s_value.ToBigEndianByteArray()); + return hkdfSha256.Expand(Encoding.UTF8.GetBytes(DerivedKeyInfo), DerivedKeySizeBytes); } /// From 564283b5e93b8f277a5c18c2634bf446e28a1ad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A1n=20Garc=C3=ADa?= Date: Tue, 13 May 2025 16:30:28 -0300 Subject: [PATCH 2/3] autover --- .DS_Store | Bin 0 -> 10244 bytes .autover/.DS_Store | Bin 0 -> 6148 bytes .autover/changes/.DS_Store | Bin 0 -> 6148 bytes .../b1e5d908-6619-420c-800e-3184a952bb45.json | 11 +++++++++++ 4 files changed, 11 insertions(+) create mode 100644 .DS_Store create mode 100644 .autover/.DS_Store create mode 100644 .autover/changes/.DS_Store create mode 100644 .autover/changes/b1e5d908-6619-420c-800e-3184a952bb45.json diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5250458f55d033743dcde7a1b29a0dd655ad7e96 GIT binary patch literal 10244 zcmeHMK~EDw6n+E6ZUL1Ty=Y7}@nRyWr6Pgop_Eo7QAlXjgPPi<4QyC;Nq0+)1k=0v z7yJv}{UQDrPx`%?q3&)=Jcwd&hRnR#+4sG9@0+(nW=ce&+-=Pf*+gWavYeSlGotW& zu0v%^k6eQkz!L?uPi^X~dco$9wiz%C7zPXjh5^HXVc=h20ROYOSW`-x)-YfgFboVB z;QhfyWm(d)ucX{M(8wbIWCp{sppSEa#MoMvwCpP>p|Gh=4?;tQK4J(3$9tRAAxm2J zl~iyN3Qj`LEc6LQ$k~Cjr8wj_fJU5IAg|A|4JTq}FYh|q|>s4(#=+&ZT)D6qc&I^932Z84t^_$K! zx7};ZTwV$MsOkFcjs&=^HdJ0dcl}n-D+gV_6-sPPy|8jtt}%1<;GkG6F3j331-TNv5J`o4u-AE+w5!AiH$4`G4FLX57U$8#e#^p<5dY)h>OW1GOUR2D+=$RD1S z^08U(|Cw3gX$4+HRD*Ak^vQ?c96=YYzI*z)GV~d%7WZqC9B8_Tpapb*Kaav>ES&GZ z#+-W2&leVv^Ye>KznSwR8A;{+H1gvSR&siJ7d56%8?;JgAnTCq$p{blJ4v7|nY}7+_mJOPNskc|(WhA6 zW)a;gW~^d{8dX!;oK10U3)#cpO#oj;SeKa;*FOiV>(YPBkhCqBySVNoEMo7#4E+7& zk=;J+q%h1?9Fqe_Rm4=GCy>YS6Gx20ZihCLc9|6s-|@B9sptGj?ejXGRF-G3BecOs zYIo;7B*Df&@6`D?sXbs2QHl|ymnnt;!+>GHFkl!k44f$j##J;n-~U&S|NZ|=#cYBx z3>XHI42VpnRw-kQuee4PzH7HpU!!tiy}ptXf<|7)L(1!TJn(h=O;pw`u;hY%Nz1;H Z7(w~#KL!l$HqH6pod09DyZ_Po{|A*J^N9ce literal 0 HcmV?d00001 diff --git a/.autover/.DS_Store b/.autover/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..107c7ec1ea7d20c31284c830cb3f241237d590e8 GIT binary patch literal 6148 zcmeHK&2HL25S|4Rj7TI7sZ`~(Qm!Zk(jZm6xP%^ht)upkRMZYAxN&!pZQy{A`~b$EewOVvP2$hH**yz$sS@T{Ty?Xm`aFcvj`qq3W2>iQ^?K@n-9R_PE%z{)UT3ut$inYpS zWCd6OR$yBcaO;y--8MB?>Cksr zSi~MQVX26gD%=%ASUR>#m*+byELu7Ucli+R$->=Ggn2rSFHJfK-y)Z+04s1`fwHYO zX#aoz`2GJriEFF?EAU?_AS(T!-@{LGd+Wx>(OxUjzoL^-USaVs1p~7cV=isQFVT%* YyQBcocUV}&7BoHx7#X-=1)f!bKO!V%>i_@% literal 0 HcmV?d00001 diff --git a/.autover/changes/.DS_Store b/.autover/changes/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..19af1f4de4b34598271db382c347609a7577e3af GIT binary patch literal 6148 zcmeHKJx>Bb5PhQ*BsP>ZCfiy-K!F_HhG1dsAJ79EL%71&U+2%(H#4j8a@bnv44HYm z^RavPiW>%iY+m7}!gk`6G zMcJ3~2AA3@;qR1_QxBF!05IejgH5F&Atd?bAVHBLHzgw+YuaOAwPf zh`C_vNDoE4l<1{WMhx+C`cuiv1zSfihuF=-r}2nMm}sq-8*RnoZ+S`A YjOT)_qm|Qd<;3_SpuB_x1HZw*2igiCbN~PV literal 0 HcmV?d00001 diff --git a/.autover/changes/b1e5d908-6619-420c-800e-3184a952bb45.json b/.autover/changes/b1e5d908-6619-420c-800e-3184a952bb45.json new file mode 100644 index 0000000..484c5ba --- /dev/null +++ b/.autover/changes/b1e5d908-6619-420c-800e-3184a952bb45.json @@ -0,0 +1,11 @@ +{ + "Projects": [ + { + "Name": "Amazon.Extensions.CognitoAuthentication", + "Type": "Patch", + "ChangelogMessages": [ + "Fix Auth with device and device confirmation" + ] + } + ] +} \ No newline at end of file From acca28a1f723490ba4de12bc2d37a38a266d4583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A1n=20Garc=C3=ADa?= Date: Fri, 16 May 2025 12:22:20 -0300 Subject: [PATCH 3/3] Removes .DS_Store files Excludes macOS .DS_Store files from the repository to prevent unnecessary clutter and potential issues. --- .DS_Store | Bin 10244 -> 0 bytes .autover/.DS_Store | Bin 6148 -> 0 bytes .autover/changes/.DS_Store | Bin 6148 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store delete mode 100644 .autover/.DS_Store delete mode 100644 .autover/changes/.DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 5250458f55d033743dcde7a1b29a0dd655ad7e96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10244 zcmeHMK~EDw6n+E6ZUL1Ty=Y7}@nRyWr6Pgop_Eo7QAlXjgPPi<4QyC;Nq0+)1k=0v z7yJv}{UQDrPx`%?q3&)=Jcwd&hRnR#+4sG9@0+(nW=ce&+-=Pf*+gWavYeSlGotW& zu0v%^k6eQkz!L?uPi^X~dco$9wiz%C7zPXjh5^HXVc=h20ROYOSW`-x)-YfgFboVB z;QhfyWm(d)ucX{M(8wbIWCp{sppSEa#MoMvwCpP>p|Gh=4?;tQK4J(3$9tRAAxm2J zl~iyN3Qj`LEc6LQ$k~Cjr8wj_fJU5IAg|A|4JTq}FYh|q|>s4(#=+&ZT)D6qc&I^932Z84t^_$K! zx7};ZTwV$MsOkFcjs&=^HdJ0dcl}n-D+gV_6-sPPy|8jtt}%1<;GkG6F3j331-TNv5J`o4u-AE+w5!AiH$4`G4FLX57U$8#e#^p<5dY)h>OW1GOUR2D+=$RD1S z^08U(|Cw3gX$4+HRD*Ak^vQ?c96=YYzI*z)GV~d%7WZqC9B8_Tpapb*Kaav>ES&GZ z#+-W2&leVv^Ye>KznSwR8A;{+H1gvSR&siJ7d56%8?;JgAnTCq$p{blJ4v7|nY}7+_mJOPNskc|(WhA6 zW)a;gW~^d{8dX!;oK10U3)#cpO#oj;SeKa;*FOiV>(YPBkhCqBySVNoEMo7#4E+7& zk=;J+q%h1?9Fqe_Rm4=GCy>YS6Gx20ZihCLc9|6s-|@B9sptGj?ejXGRF-G3BecOs zYIo;7B*Df&@6`D?sXbs2QHl|ymnnt;!+>GHFkl!k44f$j##J;n-~U&S|NZ|=#cYBx z3>XHI42VpnRw-kQuee4PzH7HpU!!tiy}ptXf<|7)L(1!TJn(h=O;pw`u;hY%Nz1;H Z7(w~#KL!l$HqH6pod09DyZ_Po{|A*J^N9ce diff --git a/.autover/.DS_Store b/.autover/.DS_Store deleted file mode 100644 index 107c7ec1ea7d20c31284c830cb3f241237d590e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK&2HL25S|4Rj7TI7sZ`~(Qm!Zk(jZm6xP%^ht)upkRMZYAxN&!pZQy{A`~b$EewOVvP2$hH**yz$sS@T{Ty?Xm`aFcvj`qq3W2>iQ^?K@n-9R_PE%z{)UT3ut$inYpS zWCd6OR$yBcaO;y--8MB?>Cksr zSi~MQVX26gD%=%ASUR>#m*+byELu7Ucli+R$->=Ggn2rSFHJfK-y)Z+04s1`fwHYO zX#aoz`2GJriEFF?EAU?_AS(T!-@{LGd+Wx>(OxUjzoL^-USaVs1p~7cV=isQFVT%* YyQBcocUV}&7BoHx7#X-=1)f!bKO!V%>i_@% diff --git a/.autover/changes/.DS_Store b/.autover/changes/.DS_Store deleted file mode 100644 index 19af1f4de4b34598271db382c347609a7577e3af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKJx>Bb5PhQ*BsP>ZCfiy-K!F_HhG1dsAJ79EL%71&U+2%(H#4j8a@bnv44HYm z^RavPiW>%iY+m7}!gk`6G zMcJ3~2AA3@;qR1_QxBF!05IejgH5F&Atd?bAVHBLHzgw+YuaOAwPf zh`C_vNDoE4l<1{WMhx+C`cuiv1zSfihuF=-r}2nMm}sq-8*RnoZ+S`A YjOT)_qm|Qd<;3_SpuB_x1HZw*2igiCbN~PV