From dc5c3d3f6a10516deba0eab2122ef3d35f7d66b9 Mon Sep 17 00:00:00 2001 From: YuNing Chen Date: Wed, 31 Jul 2024 17:21:14 +0800 Subject: [PATCH] Add Chacha crypto family (#28) * feat: add crypto method chacha family ci: fmt * chore: adapt upstream * fix(chacha_test): handle error * ci: info --- crypto/chacha.mbt | 432 +++++++++++++++++++++++++++++++++++++++++ crypto/chacha_test.mbt | 37 ++++ crypto/crypto.mbti | 6 + crypto/md5.mbt | 4 - crypto/sha1.mbt | 4 - crypto/utils.mbt | 8 + 6 files changed, 483 insertions(+), 8 deletions(-) create mode 100644 crypto/chacha.mbt create mode 100644 crypto/chacha_test.mbt diff --git a/crypto/chacha.mbt b/crypto/chacha.mbt new file mode 100644 index 0000000..fc4182c --- /dev/null +++ b/crypto/chacha.mbt @@ -0,0 +1,432 @@ +// Copyright 2024 International Digital Economy Academy +// +// Licensed 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. + +fn flipWord(word : UInt) -> UInt { + let b0 = (word & 0xff).lsl(24) + let b1 = (word & 0xff00).lsl(8) + let b2 = (word & 0xff0000).lsr(8) + let b3 = (word & 0xff000000).lsr(24) + b0 | b1 | b2 | b3 +} + +test "flipWord" { + let word = 0x12345678U + let flipped = flipWord(word) + inspect!(flipped, content="2018915346") +} + +fn quarterRound( + state : FixedArray[UInt], + w : Int, + x : Int, + y : Int, + z : Int +) -> FixedArray[UInt] { + fn fullQuarterRound( + a : UInt, + b : UInt, + c : UInt, + d : UInt + ) -> (UInt, UInt, UInt, UInt) { + quarterRound4(quarterRound3(quarterRound2(quarterRound1((a, b, c, d))))) + } + + let t = fullQuarterRound(state[w], state[x], state[y], state[z]) + state[w] = t.0 + state[x] = t.1 + state[y] = t.2 + state[z] = t.3 + state +} + +test "quarterRound" { + let state = FixedArray::make(16, 0U) + state[0] = 0x879531e0U + state[1] = 0xc5ecf37dU + state[2] = 0x516461b1U + state[3] = 0xc9a62f8aU + state[4] = 0x44c20ef3U + state[5] = 0x3390af7fU + state[6] = 0xd9fc690bU + state[7] = 0x2a5f714cU + state[8] = 0x53372767U + state[9] = 0xb00a5631U + state[10] = 0x974c541aU + state[11] = 0x359e9963U + state[12] = 0x5c971061U + state[13] = 0x3d631689U + state[14] = 0x2098d9d6U + state[15] = 0x91dbd320U + let newState = quarterRound(state, 2, 7, 8, 13) + inspect!( + newState, + content="[2274701792, 3320640381, 3182986972, 3383111562, 1153568499, 865120127, 3657197835, 3484200914, 3832277632, 2953467441, 2538361882, 899586403, 1553404001, 3435166841, 546888150, 2447102752]", + ) +} + +fn chachaBlockRound(state : FixedArray[UInt]) -> FixedArray[UInt] { + let state1 = quarterRound(state, 0, 4, 8, 12) + let state2 = quarterRound(state1, 1, 5, 9, 13) + let state3 = quarterRound(state2, 2, 6, 10, 14) + let state4 = quarterRound(state3, 3, 7, 11, 15) + let state5 = quarterRound(state4, 0, 5, 10, 15) + let state6 = quarterRound(state5, 1, 6, 11, 12) + let state7 = quarterRound(state6, 2, 7, 8, 13) + let state8 = quarterRound(state7, 3, 4, 9, 14) + state8 +} + +test "chachaBlockRound" { + let state = FixedArray::make(16, 0U) + state[0] = 0x61707865U + state[1] = 0x3320646eU + state[2] = 0x79622d32U + state[3] = 0x6b206574U + state[4] = 0x03020100U + state[5] = 0x07060504U + state[6] = 0x0b0a0908U + state[7] = 0x0f0e0d0cU + state[8] = 0x13121110U + state[9] = 0x17161514U + state[10] = 0x1b1a1918U + state[11] = 0x1f1e1d1cU + state[12] = 0x00000001U + state[13] = 0x00000000U + state[14] = 0x00000000U + state[15] = 0x00000000U + let newState = chachaBlockRound(state) + inspect!( + newState, + content="[986087425, 3489031050, 2890662805, 2683391196, 1720476390, 1116253759, 2262580386, 3212003942, 2202368212, 756352536, 496298475, 669838588, 567302638, 1860562437, 1434237441, 2097484794]", + ) +} + +fn chachaBlockLoop(state : FixedArray[UInt], n : UInt) -> FixedArray[UInt] { + match n { + 0 => state.copy() + _ => chachaBlockLoop(chachaBlockRound(state.copy()), n - 1) + } +} + +test "chachaBlockLoop" { + let count = 1U + let key = FixedArray::make(8, 0U) + key[0] = 0x00010203U + key[1] = 0x04050607U + key[2] = 0x08090a0bU + key[3] = 0x0c0d0e0fU + key[4] = 0x10111213U + key[5] = 0x14151617U + key[6] = 0x18191a1bU + key[7] = 0x1c1d1e1fU + let state = FixedArray::make(16, 0U) + state[0] = 0X61707865U + state[1] = 0X3320646eU + state[2] = 0X79622d32U + state[3] = 0X6b206574U + state[4] = flipWord(key[0]) + state[5] = flipWord(key[1]) + state[6] = flipWord(key[2]) + state[7] = flipWord(key[3]) + state[8] = flipWord(key[4]) + state[9] = flipWord(key[5]) + state[10] = flipWord(key[6]) + state[11] = flipWord(key[7]) + state[12] = count + state[13] = 0 + state[14] = 0 + state[15] = 0 + inspect!( + state, + content="[1634760805, 857760878, 2036477234, 1797285236, 50462976, 117835012, 185207048, 252579084, 319951120, 387323156, 454695192, 522067228, 1, 0, 0, 0]", + ) + let newState = chachaBlockLoop(state, 4) + inspect!( + newState, + content="[2919080465, 647515738, 898727107, 777299107, 3407982512, 2489307765, 745530666, 2053399858, 1994399329, 139328223, 3709168053, 3118545354, 4170274417, 867477305, 1393604261, 3769539545]", + ) +} + +fn flipState(state : FixedArray[UInt]) -> FixedArray[UInt] { + state.map(flipWord) +} + +fn zipWith( + op : (UInt, UInt) -> UInt, + state1 : FixedArray[UInt], + state2 : FixedArray[UInt] +) -> FixedArray[UInt] { + let result = FixedArray::make(state1.length(), 0U) + for i = 0; i < state1.length(); i = i + 1 { + result[i] = op(state1[i], state2[i]) + } + result +} + +test "zipWith" { + let state1 = FixedArray::make(4, 0U) + state1[0] = 1 + state1[1] = 2 + state1[2] = 3 + state1[3] = 4 + let state2 = FixedArray::make(4, 0U) + state2[0] = 5 + state2[1] = 6 + state2[2] = 7 + state2[3] = 8 + let result = zipWith(UInt::op_add, state1, state2) + inspect!(result, content="[6, 8, 10, 12]") +} + +/// - chacha8: round = 4 +/// - chacha12: round = 6 +/// - chacha20: round = 10 +fn chachaBlock( + key : FixedArray[UInt], + count : UInt, + nonce : UInt, + round : UInt +) -> FixedArray[UInt] { + if key.length() != 8 { + abort("Invalid key length -- key must be 256 bits") + } + let state = FixedArray::make(16, 0U) + state[0] = 0X61707865U + state[1] = 0X3320646eU + state[2] = 0X79622d32U + state[3] = 0X6b206574U + state[4] = flipWord(key[0]) + state[5] = flipWord(key[1]) + state[6] = flipWord(key[2]) + state[7] = flipWord(key[3]) + state[8] = flipWord(key[4]) + state[9] = flipWord(key[5]) + state[10] = flipWord(key[6]) + state[11] = flipWord(key[7]) + state[12] = count + state[13] = nonce + state[14] = nonce + state[15] = nonce + let mixedState = chachaBlockLoop(state.copy(), round) + let combinedState = zipWith(UInt::op_add, state, mixedState) + flipState(combinedState) +} + +test "chachaBlock" { + let key = FixedArray::make(8, 0U) + key[0] = 0x00010203U + key[1] = 0x04050607U + key[2] = 0x08090a0bU + key[3] = 0x0c0d0e0fU + key[4] = 0x10111213U + key[5] = 0x14151617U + key[6] = 0x18191a1bU + key[7] = 0x1c1d1e1fU + let keyStream = chachaBlock(key, 1, 0, 4) + inspect!( + keyStream, + content="[1981443599, 3367155801, 4121555886, 386561433, 2964333518, 2044159387, 854489399, 1047687817, 1898967689, 4077872159, 3447861240, 3864461272, 1918276088, 967291955, 2780172371, 3650858720]", + ) +} + +fn stateToBytes(state : FixedArray[UInt]) -> Bytes { + fn from_array(arr : Array[UInt]) -> Bytes { + let rv = Bytes::new(arr.length()) + for i = 0; i < arr.length(); i = i + 1 { + rv[i] = arr[i].to_byte() + } + rv + } + + let result = Array::make(64, 0U) + for i = 0; i < 16; i = i + 1 { + let word = state[i] + result[i * 4 + 3] = word & 0xff + result[i * 4 + 2] = (word & 0xff00).lsr(8) + result[i * 4 + 1] = (word & 0xff0000).lsr(16) + result[i * 4 + 0] = (word & 0xff000000).lsr(24) + } + from_array(result) +} + +test "stateToBytes" { + let state = FixedArray::make(16, 0U) + state[0] = 0x879531e0 + state[1] = 0xc5ecf37d + state[2] = 0x516461b1 + state[3] = 0xc9a62f8a + state[4] = 0x44c20ef3 + state[5] = 0x3390af7f + state[6] = 0xd9fc690b + state[7] = 0x2a5f714c + state[8] = 0x53372767 + state[9] = 0xb00a5631 + state[10] = 0x974c541a + state[11] = 0x359e9963 + state[12] = 0x5c971061 + state[13] = 0x3d631689 + state[14] = 0x2098d9d6 + state[15] = 0x91dbd320 + let bytes = stateToBytes(state) + inspect!( + bytes_to_hex_string(bytes), + content="879531e0c5ecf37d516461b1c9a62f8a44c20ef33390af7fd9fc690b2a5f714c53372767b00a5631974c541a359e99635c9710613d6316892098d9d691dbd320", + ) +} + +/// Encrypts a block of data using the ChaCha8 algorithm. +/// - [key] must be 8 32-bit unsigned integers. +/// - [counter] is the counter value. +/// - [block] is the block of data to be encrypted. +/// - [nonce] is default to 0 +/// - Returns the encrypted block of data. +pub fn chacha8( + key : FixedArray[UInt], + counter : UInt, + block : Bytes, + ~nonce : UInt = 0 +) -> Bytes!String { + if key.length() != 8 { + raise "Invalid key length -- key must be 8 32-bit unsigned integers" + } + chacha(key, counter, block, 4, nonce) +} + +/// Encrypts a block of data using the ChaCha12 algorithm. +/// - [key] must be 8 32-bit unsigned integers. +/// - [counter] is the counter value. +/// - [block] is the block of data to be encrypted. +/// - [nonce] is default to 0 +/// - Returns the encrypted block of data. +pub fn chacha12( + key : FixedArray[UInt], + counter : UInt, + block : Bytes, + ~nonce : UInt = 0 +) -> Bytes!String { + if key.length() != 8 { + raise "Invalid key length -- key must be 8 32-bit unsigned integers" + } + chacha(key, counter, block, 6, nonce) +} + +/// Encrypts a block of data using the ChaCha20 algorithm. +/// - [key] must be 8 32-bit unsigned integers. +/// - [counter] is the counter value. +/// - [block] is the block of data to be encrypted. +/// - [nonce] is default to 0 +/// - Returns the encrypted block of data. +pub fn chacha20( + key : FixedArray[UInt], + counter : UInt, + block : Bytes, + ~nonce : UInt = 0 +) -> Bytes!String { + if key.length() != 8 { + raise "Invalid key length -- key must be 8 32-bit unsigned integers" + } + chacha(key, counter, block, 10, nonce) +} + +fn chacha( + key : FixedArray[UInt], + counter : UInt, + block : Bytes, + round : UInt, + nonce : UInt +) -> Bytes { + if block.length() == 0 { + return Bytes::new(0) + } + fn takeToFixedArray(b : Bytes, len : Int) -> FixedArray[Byte] { + let result = FixedArray::make(len, b'0') + for i = 0; i < len; i = i + 1 { + result[i] = b[i] + } + result + } + + fn zipWith( + op : (Byte, Byte) -> Byte, + state1 : FixedArray[Byte], + state2 : FixedArray[Byte] + ) -> Bytes { + let result = Array::make(state1.length(), b'0') + for i = 0; i < state1.length(); i = i + 1 { + result[i] = op(state1[i], state2[i]) + } + Bytes::from_array(result) + } + + fn dropBytes(b : Bytes, len : Int) -> Bytes { + let result = Bytes::new(b.length() - len) + for i = 0; i < b.length() - len; i = i + 1 { + result[i] = b[len + i] + } + result + } + + fn concatBytes(b1 : Bytes, b2 : Bytes) -> Bytes { + let result = Bytes::new(b1.length() + b2.length()) + for i = 0; i < b1.length(); i = i + 1 { + result[i] = b1[i] + } + for i = 0; i < b2.length(); i = i + 1 { + result[b1.length() + i] = b2[i] + } + result + } + + let keyStream = chachaBlock(key, counter, nonce, round) + let pad = stateToBytes(keyStream) + let len = if block.length() < 64 { block.length() } else { 64 } + let maskedBlock = zipWith( + Byte::lxor, + takeToFixedArray(pad, len), + takeToFixedArray(block, len), + ) + let res = concatBytes( + maskedBlock, + chacha(key, counter + 1, dropBytes(block, len), round, nonce), + ) + res +} + +fn quarterRound1(t : (UInt, UInt, UInt, UInt)) -> (UInt, UInt, UInt, UInt) { + let aprime = t.0 + t.1 + let dprime = t.3 ^ aprime + let dout = rotate_left_u(dprime, 16) + (aprime, t.1, t.2, dout) +} + +fn quarterRound2(t : (UInt, UInt, UInt, UInt)) -> (UInt, UInt, UInt, UInt) { + let cprime = t.2 + t.3 + let bprime = t.1 ^ cprime + let bout = rotate_left_u(bprime, 12) + (t.0, bout, cprime, t.3) +} + +fn quarterRound3(t : (UInt, UInt, UInt, UInt)) -> (UInt, UInt, UInt, UInt) { + let aprime = t.0 + t.1 + let dprime = t.3 ^ aprime + let dout = rotate_left_u(dprime, 8) + (aprime, t.1, t.2, dout) +} + +fn quarterRound4(t : (UInt, UInt, UInt, UInt)) -> (UInt, UInt, UInt, UInt) { + let cprime = t.2 + t.3 + let bprime = t.1 ^ cprime + let bout = rotate_left_u(bprime, 7) + (t.0, bout, cprime, t.3) +} diff --git a/crypto/chacha_test.mbt b/crypto/chacha_test.mbt new file mode 100644 index 0000000..ddd20a1 --- /dev/null +++ b/crypto/chacha_test.mbt @@ -0,0 +1,37 @@ +// Copyright 2024 International Digital Economy Academy +// +// Licensed 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. + +test "chachaEncrypt" { + let key = FixedArray::make(8, 0U) + key[0] = 0 + key[1] = 0 + key[2] = 0 + key[3] = 0 + key[4] = 0 + key[5] = 0 + key[6] = 0 + key[7] = 0 + let plaintext = "abc" + let block = plaintext.to_bytes() + inspect!(bytes_to_hex_string(block), content="610062006300") + let encrypted = @crypto.chacha8!(key, 0, block) + let expected = "5f008d2fea5f" + @test.eq!(bytes_to_hex_string(encrypted), expected) + let encrypted = @crypto.chacha12!(key, 0, block) + let expected = "faf4f86a6455" + @test.eq!(bytes_to_hex_string(encrypted), expected) + let encrypted = @crypto.chacha20!(key, 0, block) + let expected = "17b882adc3f1" + @test.eq!(bytes_to_hex_string(encrypted), expected) +} diff --git a/crypto/crypto.mbti b/crypto/crypto.mbti index eff510a..7388eb3 100644 --- a/crypto/crypto.mbti +++ b/crypto/crypto.mbti @@ -3,6 +3,12 @@ package moonbitlang/x/crypto // Values fn bytes_to_hex_string(Bytes) -> String +fn chacha12(FixedArray[UInt], UInt, Bytes, ~nonce : UInt = ..) -> Bytes!String + +fn chacha20(FixedArray[UInt], UInt, Bytes, ~nonce : UInt = ..) -> Bytes!String + +fn chacha8(FixedArray[UInt], UInt, Bytes, ~nonce : UInt = ..) -> Bytes!String + fn md5(Bytes) -> Bytes fn sha1(Bytes) -> Bytes diff --git a/crypto/md5.mbt b/crypto/md5.mbt index 7108b4e..eadee94 100644 --- a/crypto/md5.mbt +++ b/crypto/md5.mbt @@ -120,10 +120,6 @@ fn md5_update(ctx : MD5Context, data : Bytes) -> Unit { } } -fn rotate_left_u(x : UInt, n : Int) -> UInt { - x.lsl(n).lor(x.lsr(32 - n)) -} - fn md5_transform(state : FixedArray[UInt], input : FixedArray[UInt]) -> Unit { let a = Ref::new(state[0]) let b = Ref::new(state[1]) diff --git a/crypto/sha1.mbt b/crypto/sha1.mbt index 3fe6fc9..0cde33a 100644 --- a/crypto/sha1.mbt +++ b/crypto/sha1.mbt @@ -78,7 +78,3 @@ pub fn sha1(input : Bytes) -> Bytes { } result } - -fn rotate_left(x : Int, n : Int) -> Int { - x.lsl(n).lor(x.lsr(32 - n)) -} diff --git a/crypto/utils.mbt b/crypto/utils.mbt index bd658fe..f10a77c 100644 --- a/crypto/utils.mbt +++ b/crypto/utils.mbt @@ -47,3 +47,11 @@ fn to_hex_char(n : Int) -> String { _ => abort("invalid hex digit") } } + +fn rotate_left(x : Int, n : Int) -> Int { + x.lsl(n).lor(x.lsr(32 - n)) +} + +fn rotate_left_u(x : UInt, n : Int) -> UInt { + x.lsl(n) | x.lsr(32 - n) +}