// Cryptol AES Implementation // Copyright (c) 2010-2013, Galois Inc. // www.cryptol.net // You can freely use this source code for educational purposes. // This is a fairly close implementation of the FIPS-197 standard: // http://csrc.nist.gov/publications/fips/fips197/fips-197.pdf // Nk: Number of blocks in the key // Must be one of 4 (AES128), 6 (AES192), or 8 (AES256) // Aside from this line, no other code below needs to change for // implementing AES128, AES192, or AES256 module AES where type AES128 = 4 type AES192 = 6 type AES256 = 8 type Nk = AES128 // For Cryptol 2.x | x > 0 // NkValid: `Nk -> Bit // property NkValid k = (k == `AES128) || (k == `AES192) || (k == `AES256) // Number of blocks and Number of rounds type Nb = 4 type Nr = 6 + Nk type AESKeySize = (Nk*32) // Helper type definitions type GF28 = [8] type State = [4][Nb]GF28 type RoundKey = State type KeySchedule = (RoundKey, [Nr-1]RoundKey, RoundKey) // GF28 operations gf28Add : {n} (fin n) => [n]GF28 -> GF28 gf28Add ps = sums ! 0 where sums = [zero] # [ p ^ s | p <- ps | s <- sums ] irreducible = <| x^^8 + x^^4 + x^^3 + x + 1 |> gf28Mult : (GF28, GF28) -> GF28 gf28Mult (x, y) = pmod(pmult x y) irreducible gf28Pow : (GF28, [8]) -> GF28 gf28Pow (n, k) = pow k where sq x = gf28Mult (x, x) odd x = x ! 0 pow i = if i == 0 then 1 else if odd i then gf28Mult(n, sq (pow (i >> 1))) else sq (pow (i >> 1)) gf28Inverse : GF28 -> GF28 gf28Inverse x = gf28Pow (x, 254) gf28DotProduct : {n} (fin n) => ([n]GF28, [n]GF28) -> GF28 gf28DotProduct (xs, ys) = gf28Add [ gf28Mult (x, y) | x <- xs | y <- ys ] gf28VectorMult : {n, m} (fin n) => ([n]GF28, [m][n]GF28) -> [m]GF28 gf28VectorMult (v, ms) = [ gf28DotProduct(v, m) | m <- ms ] gf28MatrixMult : {n, m, k} (fin m) => ([n][m]GF28, [m][k]GF28) -> [n][k]GF28 gf28MatrixMult (xss, yss) = [ gf28VectorMult(xs, yss') | xs <- xss ] where yss' = transpose yss // The affine transform and its inverse xformByte : GF28 -> GF28 xformByte b = gf28Add [b, (b >>> 4), (b >>> 5), (b >>> 6), (b >>> 7), c] where c = 0x63 xformByte' : GF28 -> GF28 xformByte' b = gf28Add [(b >>> 2), (b >>> 5), (b >>> 7), d] where d = 0x05 // The SubBytes transform and its inverse SubByte : GF28 -> GF28 SubByte b = xformByte (gf28Inverse b) SubByte' : GF28 -> GF28 SubByte' b = sbox@b SubBytes : State -> State SubBytes state = [ [ SubByte' b | b <- row ] | row <- state ] InvSubByte : GF28 -> GF28 InvSubByte b = gf28Inverse (xformByte' b) InvSubBytes : State -> State InvSubBytes state =[ [ InvSubByte b | b <- row ] | row <- state ] // The ShiftRows transform and its inverse ShiftRows : State -> State ShiftRows state = [ row <<< shiftAmount | row <- state | shiftAmount <- [0 .. 3] ] InvShiftRows : State -> State InvShiftRows state = [ row >>> shiftAmount | row <- state | shiftAmount <- [0 .. 3] ] // The MixColumns transform and its inverse MixColumns : State -> State MixColumns state = gf28MatrixMult (m, state) where m = [[2, 3, 1, 1], [1, 2, 3, 1], [1, 1, 2, 3], [3, 1, 1, 2]] InvMixColumns : State -> State InvMixColumns state = gf28MatrixMult (m, state) where m = [[0x0e, 0x0b, 0x0d, 0x09], [0x09, 0x0e, 0x0b, 0x0d], [0x0d, 0x09, 0x0e, 0x0b], [0x0b, 0x0d, 0x09, 0x0e]] // The AddRoundKey transform AddRoundKey : (RoundKey, State) -> State AddRoundKey (rk, s) = rk ^ s // Key expansion Rcon : [8] -> [4]GF28 Rcon i = [(gf28Pow (<| x |>, i-1)), 0, 0, 0] SubWord : [4]GF28 -> [4]GF28 SubWord bs = [ SubByte b | b <- bs ] RotWord : [4]GF28 -> [4]GF28 RotWord [a0, a1, a2, a3] = [a1, a2, a3, a0] NextWord : ([8],[4][8],[4][8]) -> [4][8] NextWord(i, prev, old) = old ^ mask where mask = if i % `Nk == 0 then SubWord(RotWord(prev)) ^ Rcon (i / `Nk) else if (`Nk > 6) && (i % `Nk == 4) then SubWord(prev) else prev ExpandKeyForever : [Nk][4][8] -> [inf]RoundKey ExpandKeyForever seed = [ transpose g | g <- groupBy`{4} (keyWS seed) ] keyWS : [Nk][4][8] -> [inf][4][8] keyWS seed = xs where xs = seed # [ NextWord(i, prev, old) | i <- [ `Nk ... ] | prev <- drop`{Nk-1} xs | old <- xs ] checkKey = take`{16} (drop`{8} (keyWS ["abcd", "defg", "1234", "5678"])) checkKey2 = [transpose g | g <- groupBy`{4}checkKey] ExpandKey : [AESKeySize] -> KeySchedule ExpandKey key = (keys @ 0, keys @@ [1 .. (Nr - 1)], keys @ `Nr) where seed : [Nk][4][8] seed = split (split key) keys = ExpandKeyForever seed fromKS : KeySchedule -> [Nr+1][4][32] fromKS (f, ms, l) = [ formKeyWords (transpose k) | k <- [f] # ms # [l] ] where formKeyWords bbs = [ join bs | bs <- bbs ] // AES rounds and inverses AESRound : (RoundKey, State) -> State AESRound (rk, s) = AddRoundKey (rk, MixColumns (ShiftRows (SubBytes s))) AESFinalRound : (RoundKey, State) -> State AESFinalRound (rk, s) = AddRoundKey (rk, ShiftRows (SubBytes s)) AESInvRound : (RoundKey, State) -> State AESInvRound (rk, s) = InvMixColumns (AddRoundKey (rk, InvSubBytes (InvShiftRows s))) AESFinalInvRound : (RoundKey, State) -> State AESFinalInvRound (rk, s) = AddRoundKey (rk, InvSubBytes (InvShiftRows s)) // Converting a 128 bit message to a State and back msgToState : [128] -> State msgToState msg = transpose (split (split msg)) stateToMsg : State -> [128] stateToMsg st = join (join (transpose st)) // AES Encryption aesEncrypt : ([128], [AESKeySize]) -> [128] aesEncrypt (pt, key) = stateToMsg (AESFinalRound (kFinal, rounds ! 0)) where (kInit, ks, kFinal) = ExpandKey key state0 = AddRoundKey(kInit, msgToState pt) rounds = [state0] # [ AESRound (rk, s) | rk <- ks | s <- rounds ] // AES Decryption aesDecrypt : ([128], [AESKeySize]) -> [128] aesDecrypt (ct, key) = stateToMsg (AESFinalInvRound (kFinal, rounds ! 0)) where (kFinal, ks, kInit) = ExpandKey key state0 = AddRoundKey(kInit, msgToState ct) rounds = [state0] # [ AESInvRound (rk, s) | rk <- reverse ks | s <- rounds ] sbox : [256]GF28 sbox = [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16] // Test runs: // cryptol> aesEncrypt (0x3243f6a8885a308d313198a2e0370734, \ // 0x2b7e151628aed2a6abf7158809cf4f3c) // 0x3925841d02dc09fbdc118597196a0b32 // cryptol> aesEncrypt (0x00112233445566778899aabbccddeeff, \ // 0x000102030405060708090a0b0c0d0e0f) // 0x69c4e0d86a7b0430d8cdb78070b4c55a property AESCorrect msg key = aesDecrypt (aesEncrypt (msg, key), key) == msg // Benchmark: type nblocks = 128 property bench_correct = bench bench_data == bench_result bench : [128 * nblocks] -> [128 * nblocks] bench data = join [ aesEncrypt (block, key) | block <- split data ] where key = 0x3243f6a8885a308d313198a2e0370734 bench_data : [128 * nblocks] bench_data = //random 0 0xcddf97f18ad18da94ae27558e975608f673c896a718cffbc90c746160a003d540e353ea1a32cf650c25298cf353b36849f68360e07ad40a9e6c0e4dd2351dce8c06dd82c27642a5e9ce3804780d531af41768b4697b45383d58dfd98c9f2e6d5788e671229529d239b40fc9a52436c437e716cef3c5503d567eff3c2f35d806ae4431455ec096526b1b584cb4a80efde3174361e912a46bf8b7b8d3ca4cebacea935ccd766976614885f5330441ca4acee37c9728fb53708042d9952d8ef3ca544c870a7ee689f8b6d78764368e849274946d0e8bdc69f4a4004cbbce034f1d0a6f8447a756a5f9c217e377909d0c4bde859732e7263c03013c623cb1f2478b77f7838b3d3581e0aba9da951dd18466a131bca89252fc17b9bbe475530d425ac7a79cbc26a941dcc16ded680dddada735c76fa469ebeadf1c8fc33a2c7dc00b865eaec95f1448583425302a9023d39c3bb794685a5e30f196f7c0bdc2b8790d35f8bb9c4359e17ca53e8450da4db030bb67fb4cab68ef4a5edfbe120f1c9824b4faa0cc767bb7304238a798534f065cc1cc8fe310d76c2b440b64348a8e16873eddb5931313573c2cb43a47c2ff0f9c8ec264a0a6ec6474c1056fc7f376c01e5d3b6fea382b5086c7c80bb4c5505f2d4f18dc01cff4baa71eac658ca78e5acbde546dbe85dd71708fb46c8ec00ef75b9b577f93a3c550781a642d5bc4fa2da325656f737d272875b9185fe86a0e0e3eefaf51294d0f06340db93715c99df443e286c0d1e2ae869a7e0d705d6369362a220617ac4803fd205de679ea6ed82881cc2315d73c9cc8f4512ee61afaf127eac098d1d31b075c16aa902024594337c6b2a290cedcbd44190105d20de7ef16fb310400ccbc9ca6a1f4de0a9b1f82b780ee3eda52af664b883c32dd70c860905c0f9e83ee0ae5fc016b81c4c4ca70c05703035637e4988a827bf6e230ba30dc78b8c5663e14273827fb6c4aa700b95a04f1456ce15d18740ee79b7aeb85feffac9f5bd54c9a9bd494a9fc8fab4280316ac8f6552849a798e2b5d7326bb2208fdf2cf6b372311edcd87bb2b1805afd1b6e085fad1a28bc4578e1abea57227f49c141d7d08893d8083249e32cc9645748684cc5d4d492abbdaf5f8373b5e2e4bd91c15346e1d455415395bc0342185665ebeb94fda5fdf7e601961bf1f1109373e935a077a0088980844ce1c87f347380f3c805b01407cf9154f5818db41a6f5d87994df790421a9dbf7d56c11a5e723ec853f32bcf7dc0eef03bbf164564167c6212c35ddd9d6112eb43e4826d1da8f065b804fd48b5ff863b4f4246ba6ebaef90396843e084168d6826abf5f6b0e82c7a7a96707e650e86f82862c5910e0c4d6a48182656e0e76be4017c739feb2da56bb69db4b0885c772180607ce880b15064b5b9878cd1c3d4ae9657c5fdf5ca5d7755807d74e2a57aed4e9fe90c49ed4f01ae3cb812ada6b15c7009d98930d8a41cf23e8f5a962e93c8cecef6044cfcc8843d1f6b5dfb868de036fa5d992c861f056f504f54e8d077028143a2807676c02faf35435ee43cb1ece1a82c7f142be774c824b7e8e3fcad737f58f4818994842ab40a211f9569ad476beef0f2f97be0fb515cf0754641748b2af38d58937c3428a147647911734d54bf06be7f3fcab19b874ff52893af6a45c44adf11c17bf177fc0e90539327373e6594e249aca3a386272d1405455e7e8b463029c0460a31b59b9dc1a15d18cdf1997df250721735651c5e7de7059cb755aa4a0c99962e6485b2ccb02ffecb022fc867e36a63ef77ee8740af2e6f25b0d497d3bcc213a939a47a64057caf107e79661d15f469f8e32b2175bed98af21776a9a2cde4e4982ecd695b4dcef8822806cf74ac73aa5b9d8e6dc0a6d2b97b75d11553a9478296631b7a3c340113247f32bf7a7c42e85b9e517b4f9a09ab453de795f156f09d2704bfa56f5ade38e0eae826796bd54165967332985ecdd4991b8e2bb4016e0d2da173690feace03245c2ef868b44ea7892c0ce15d818a32e5bf57d53a1d86cbb3074221083cfa570c17104da26c063b3a349ce35facd3b7bf267383235a5620217d58201c74105302f3445e024313338fe93dd6f617088b41c836083ceae512782e458c4f5c74bd1987ccb098d1d89fe63a5e5881e56b0c5aba87ef2c5e8d6333d91e9dcbdc45a3c16d67b32c4e51e95231aa7e0b9342c599ebdc6ebb6e4dce1fcd98add42d6ab08707a6f5a38154ad5a3674e8a05105c5ecec180f9661122da31a94e9ad7d337ff5ab4a28a5c5cba9a393f484ed5e5f37bbb4b7caecbf9cbb432cae0b2f6bd5ce6068104f012d6428a9b172847e18f12708de6248cdf0401c865292645fb30114f4f4b53d4473b6ffc53ae0870e85c24f631f52c04d227ea9bc0a59828b6f9eafd95cac13ccfb1cdbae3550b0cd99e1812346d5d01b5a782d1d50fbedf858a4a044fd9384e3a6c10cb4227e276c7399b897b9dcbd2a5cb4e14d8341dd32029938f444fa3dcf2d23198f6bf042439cf96a534f3041774a6c3d5b6bb5bfcd8d7af57402431c7dd758da93ccef39495977cece58087ecc80b278d3e9966b7bd8b183f0379f28aa3c9a885065b8a090f3af15a15acb553b36a73d25a581e7f54f5e1f6f49c4f638ce40ba67629e910a04444d5f6b66c4548b611f851feb08d64fa4d99b83ad218d182d0e86e7f87d4923599a547effed6b9c86e853aef9e60767bff33de916bd799eaf3922ae80abadcc91c95f47e702e2fbd2631d0b77ef85f092204141250c4162b0369be64c1d6bdc2d58c02981cc1de13ae3efba34fbfb3dd0ef3ac4c502a1b87c6ee6bc1a9131b098a85d31560c60c599398d0bd80b37bf4d20df81522b2acc749178fb785 bench_result : [128 * nblocks] bench_result = 0x9b00ae426bcc2cd6150a0af62b8be77fbb389c5b061a893588d1918f50f1f31ea1183bd81fd7faae77b4f6321a17130f46e21a2653b1f7dc520bf13305c5e7141cee9d8809d58b9b8ec2aa225120ea6ecec21fba09678bbbeda0b483ad299a8adb7b306599531cf717fd67a1c25e2adeeb48521619991e122b053c3a842936b14b6eea74734a6fc2abea6c1fc4780b2df8059ce9715299eeff7b6577409ebae71285929379cd065df0c249f9696e1b28da476ae52d55d0b1f676c619271d37d4211906d402e4eaf4df3031be5bc00962b7747e7b880bf55bee2882e5008e1c1fed70beb7e54be0545100a2e122b94536b888aaea25dde9e0715dfc892dee2b4fb8e94c6b15a2e77adad1f98e50ffef837309998fcdfd9bcb3d16dcd2a162a3b66c2533474981ba72321aaa9a611c670015fa6cbf9f7d7f26b3da415eccf01872cc3a686f659c0cc0d1d08a1d41470b0ceab527bd6499433a2f2df865982b3f616c246fe49f3a15b676983f7f853f6355bc2f4cdc39e5a29347f7031ab7d2659d0ddfb259fbdfe37eaee2e4dcbdfe0ac584038c5a98d85182ca2424f0e75b7f84d512828ed20bbd05065ed4ba0b850b51c31ecb231f2c879993038d7c9487e0fa46a84a02d4f5408faabd9f41edbbef5d6183dd880ea5b7272a2c46900e02357550c036f4b84168a3e891cd8fe33c2d521ce060a2863bb735e97614d0f5bf40068bfacb02297351db4a5bd80d8140a7e0734550967b2445d4236411e04f83e7f15f5148c9d758994cb8427238cea307ddde786dd74e5565b1dd905d085ebb5a7c725d72164adeeafd7387636c31eee2e729bd0fdf95686f957befdb190101cc23cc9b8e39c652e937bd1e21adad99b86d3b2ed0e4ed4ea4bd9d9ed2ad2b99adf40577fba6b25364cd6d96f79cb0f24ae551021904b57c1d469cfac780ea8469c530ab9d2fe6e98c270c5e1babed259878f48ab5824ebf327d8d18fd2e460334cc0ca9f3b0208c0b322a074185830c460cf58c45cef2234aa5ceb6ac294325bd4d4f9e569531165b9e731c84ffc92f453ce92464658592396f83555284a5f69288e3263d7bd083fa3257b0a11a9d8e4702ed06ebd172eacb7f559527637e0259573b1723c079465d593d5e89163e187ae7ac629ed75e398274daf9c420cac2d3b8f0de82dc9d50cd85d93460213416e3e5c0960c563d26fdb56e1e0dc79b251e95364389f6acbd78edd2664be1edc789b2c7a45b2101c69b3cdb8f9f6a2d7316d2cdca02fb5119d76c9bc93f7bc4e075eee1d453fa4668f368919d14035a7ba293d9787744f44e734443e9abc79a8d7aea71918f0a925026809cf43ae2fd1f6ea00fae2f87d4e7d83e4e86c81695d77e48d5f7080d61d878bde080cec46f4ed19d78f2b0c14db0bb6c871e6506064aa1c7b23c7d2baa9b5db5be3fc94923e09fddc13cd8322d05d990b3a9c5ce418c542eb80b1839a23ed7bc0b588ec957db3df1e1389ff0d541ec2f5331ab92fa7f85efa5ae9ec1c513365df179bb29d4f1914940c68468bd12e9fce04c8d08b6d3f1a4e2d632303ca99656bb248efbb6becad7ac6535678d6534aec746fbca6ec3dec85e4db505872e88bde65214b92eba09b91aff63c5db7e440a1f0ef0bca38759f9aaa35c0b8c565ec4307cc07a3226c992163aaa968ab9b9e507fa4b1910d1c24442615ffea299a8d7fcaf7aa2db3fac83d9f3b8a90bd9ae9100167944e01a07c011d0115870f5079d991d3dccd71fdd23b383b69409a1ba519c22194de6d1048e1195a4c716e0b27055aef816218d94972177732b0abd9df432d8f09293d8b22cc83e05522f7fa46742cd29b63357096601137130dc772717f8f4d02b5651fae2d74f72b25e9266090b95bf3ecb6ddda78edfa47346cf336e79548f9ea09eebe95da8903c86af35cb0abdaa4b78f05b2ef66342072b229a7f030e5b2f8e2ead7a8f5ea2a512cf0f02a4b0fc23e5aef4518f1c40674f5552a040cddf2daf47eec38103e6703720bdf40d68f8de4a9c9a39b1a21a2ea15337772526128abb3234bdce85dd7187a827f7aed625b862887f4ef41f656cc76c5311e34e654e0babe2230d3c1f7554106bab1dcdf18361e5579cf794af967421a1f06f80cfc254a9fa0a5b2a7bebd6dc8bbbf178f9647e356a4fd61e41301386784ba90dfaf5823a0007e50e61a0c3720b9b54edb800ba805e3817cddc7015febed997de87030f3341fad7249ce4530d55f5e0b3aac5a9de0d72c6c5672bc29440354d3a7d88873abdd090f41fa1db8740736583f74c7bfe526b283274f4bc08dea4884ccd75d34df9d8410f7e488b541f467331c5e7da352faa1019c394535472aa0f1ed1512928591e0b4335271df7a7d2d9d7fa1f7f4522be394b39bf15a7bc7a97ad21e171a638eb27d44efd57921938ed70335e3f3a8922553392db8a07e02cd4dc1184edbfbadaf09f97c50268d25dd5e7064968fde486d68b1051cef118d80c7a2c911a8cad22b26fd94f559cc12972b655ce014914cd3c20542815a0ff98ee166611eb2edc147bf2989d4fa1d72d88f7d211d05fe8e312b441edb3627982da6606d0cfa1c696979af5ff371e293c0a749f172343ab5f87ccd8a9d1c364f032e763a939f0696989a5316b48df43763d42170f598326579acba84e16508b5d4ffac1723c9bb98f7736e961c67caf3243b07760b3be602be9995a1a6b8c5360357e9fefab445229ccddd6214476a2c3af0eeadcd067db77ecaf7c84d605b99c89ab583edac68c3c8d951cce207c2df274709fe2d25477f14a8e50bc05f82b2cf49d9b56d97182b90a9395d90f23b9ee734d70d9312c9f6278f0ecd5f90c82f67693a589a94a6555f3abd8eac5408259879603acdc67b6fb