├── .github ├── dependabot.yml └── workflows │ ├── ci-version.yml │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── rustfmt.toml ├── src └── lib.rs └── tests └── tests.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/ci-version.yml: -------------------------------------------------------------------------------- 1 | name: CI-version 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | tests: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: 17 | - ubuntu-latest 18 | - macos-latest 19 | - windows-latest 20 | toolchain: 21 | - stable 22 | - nightly 23 | features: 24 | - 25 | name: Test ${{ matrix.toolchain }} on ${{ matrix.os }} (${{ matrix.features }}) 26 | runs-on: ${{ matrix.os }} 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: actions-rust-lang/setup-rust-toolchain@v1 30 | with: 31 | toolchain: ${{ matrix.toolchain }} 32 | - run: cargo test --release ${{ matrix.features }} 33 | - run: cargo doc --release ${{ matrix.features }} 34 | 35 | MSRV: 36 | strategy: 37 | fail-fast: false 38 | matrix: 39 | os: 40 | - ubuntu-latest 41 | - macos-latest 42 | - windows-latest 43 | toolchain: 44 | - 1.56 45 | features: 46 | - 47 | name: Test ${{ matrix.toolchain }} on ${{ matrix.os }} (${{ matrix.features }}) 48 | runs-on: ${{ matrix.os }} 49 | steps: 50 | - uses: actions/checkout@v4 51 | - uses: actions-rust-lang/setup-rust-toolchain@v1 52 | with: 53 | toolchain: ${{ matrix.toolchain }} 54 | - run: cargo test --release --lib --bins ${{ matrix.features }} -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [ push, pull_request ] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | rustfmt: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions-rust-lang/setup-rust-toolchain@v1 14 | with: 15 | toolchain: nightly 16 | components: rustfmt 17 | - uses: actions-rust-lang/rustfmt@v1 18 | 19 | clippy: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions-rust-lang/setup-rust-toolchain@v1 24 | with: 25 | components: clippy 26 | - run: cargo clippy --all-targets --all-features -- -D warnings 27 | 28 | tests: 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | os: 33 | - ubuntu-latest 34 | - macos-latest 35 | - windows-latest 36 | toolchain: 37 | - stable 38 | - nightly 39 | features: 40 | - 41 | name: Test ${{ matrix.toolchain }} on ${{ matrix.os }} (${{ matrix.features }}) 42 | runs-on: ${{ matrix.os }} 43 | steps: 44 | - uses: actions/checkout@v4 45 | - uses: actions-rust-lang/setup-rust-toolchain@v1 46 | with: 47 | toolchain: ${{ matrix.toolchain }} 48 | - run: cargo test ${{ matrix.features }} 49 | - run: cargo doc ${{ matrix.features }} 50 | 51 | MSRV: 52 | strategy: 53 | fail-fast: false 54 | matrix: 55 | os: 56 | - ubuntu-latest 57 | - macos-latest 58 | - windows-latest 59 | toolchain: 60 | - 1.56 61 | features: 62 | - 63 | name: Test ${{ matrix.toolchain }} on ${{ matrix.os }} (${{ matrix.features }}) 64 | runs-on: ${{ matrix.os }} 65 | steps: 66 | - uses: actions/checkout@v4 67 | - uses: actions-rust-lang/setup-rust-toolchain@v1 68 | with: 69 | toolchain: ${{ matrix.toolchain }} 70 | - run: cargo test --lib --bins ${{ matrix.features }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/intellij+all 2 | 3 | ### Intellij+all ### 4 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 5 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 6 | 7 | # User-specific stuff 8 | .idea/**/workspace.xml 9 | .idea/**/tasks.xml 10 | .idea/**/usage.statistics.xml 11 | .idea/**/dictionaries 12 | .idea/**/shelf 13 | 14 | # Sensitive or high-churn files 15 | .idea/**/dataSources/ 16 | .idea/**/dataSources.ids 17 | .idea/**/dataSources.local.xml 18 | .idea/**/sqlDataSources.xml 19 | .idea/**/dynamic.xml 20 | .idea/**/uiDesigner.xml 21 | .idea/**/dbnavigator.xml 22 | 23 | # Gradle 24 | .idea/**/gradle.xml 25 | .idea/**/libraries 26 | 27 | # Gradle and Maven with auto-import 28 | # When using Gradle or Maven with auto-import, you should exclude module files, 29 | # since they will be recreated, and may cause churn. Uncomment if using 30 | # auto-import. 31 | # .idea/modules.xml 32 | # .idea/*.iml 33 | # .idea/modules 34 | 35 | # CMake 36 | cmake-build-*/ 37 | 38 | # Mongo Explorer plugin 39 | .idea/**/mongoSettings.xml 40 | 41 | # File-based project format 42 | *.iws 43 | 44 | # IntelliJ 45 | out/ 46 | 47 | # mpeltonen/sbt-idea plugin 48 | .idea_modules/ 49 | 50 | # JIRA plugin 51 | atlassian-ide-plugin.xml 52 | 53 | # Cursive Clojure plugin 54 | .idea/replstate.xml 55 | 56 | # Crashlytics plugin (for Android Studio and IntelliJ) 57 | com_crashlytics_export_strings.xml 58 | crashlytics.properties 59 | crashlytics-build.properties 60 | fabric.properties 61 | 62 | # Editor-based Rest Client 63 | .idea/httpRequests 64 | 65 | ### Intellij+all Patch ### 66 | # Ignores the whole .idea folder and all .iml files 67 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 68 | 69 | .idea/ 70 | 71 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 72 | 73 | *.iml 74 | modules.xml 75 | .idea/misc.xml 76 | *.ipr 77 | 78 | 79 | # End of https://www.gitignore.io/api/intellij+all 80 | 81 | 82 | ### Rust ### 83 | # Generated by Cargo 84 | # will have compiled files and executables 85 | /target/ 86 | 87 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 88 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 89 | Cargo.lock 90 | 91 | # These are backup files generated by rustfmt 92 | **/*.rs.bk 93 | 94 | 95 | # End of https://www.gitignore.io/api/rust 96 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "short-crypt" 3 | version = "1.0.28" 4 | authors = ["Magic Len "] 5 | edition = "2021" 6 | rust-version = "1.56" 7 | repository = "https://github.com/magiclen/rust-short-crypt" 8 | homepage = "https://magiclen.org/short-crypt" 9 | keywords = ["crypto", "qrcode", "url", "encrypt", "decrypt"] 10 | categories = ["no-std", "algorithms", "cryptography"] 11 | description = "ShortCrypt is a very simple encryption library, which aims to encrypt any data into something random at first glance." 12 | license = "MIT" 13 | include = ["src/**/*", "Cargo.toml", "README.md", "LICENSE"] 14 | 15 | [dependencies] 16 | crc-any = "2.3" 17 | base64-url = "2" 18 | base32 = "0.4" 19 | debug-helper = "0.3" 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 magiclen.org (Ron Li) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ShortCrypt 2 | ==================== 3 | 4 | [![CI](https://github.com/magiclen/rust-short-crypt/actions/workflows/ci.yml/badge.svg)](https://github.com/magiclen/rust-short-crypt/actions/workflows/ci.yml) 5 | 6 | ShortCrypt is a very simple encryption library, which aims to encrypt any data into something random at first glance. 7 | Even if these data are similar, the ciphers are still pretty different. 8 | The most important thing is that a cipher is only **4 bits** larger than its plaintext so that it is suitable for data used in a URL or a QR Code. Besides these, it is also an ideal candidate for serial number generation. 9 | 10 | ## Examples 11 | 12 | `encrypt` method can create a `Cipher` tuple separating into a **base** and a **body** of the cipher. The size of a **base** is 4 bits, and the size of a **body** is equal to the plaintext. 13 | 14 | ```rust 15 | use short_crypt::ShortCrypt; 16 | 17 | let sc = ShortCrypt::new("magickey"); 18 | 19 | assert_eq!((8, [216, 78, 214, 199, 157, 190, 78, 250].to_vec()), sc.encrypt("articles")); 20 | assert_eq!("articles".as_bytes().to_vec(), sc.decrypt(&(8, vec![216, 78, 214, 199, 157, 190, 78, 250])).unwrap()); 21 | 22 | ``` 23 | 24 | `encrypt_to_url_component` method is common for encryption in most cases. After ShortCrypt `encrypt` a plaintext, it encodes the cipher into a random-like string based on Base64-URL format so that it can be concatenated with URLs. 25 | 26 | ```rust 27 | use short_crypt::ShortCrypt; 28 | 29 | let sc = ShortCrypt::new("magickey"); 30 | 31 | assert_eq!("2E87Wx52-Tvo", sc.encrypt_to_url_component("articles")); 32 | assert_eq!("articles".as_bytes().to_vec(), sc.decrypt_url_component("2E87Wx52-Tvo").unwrap()); 33 | ``` 34 | 35 | `encrypt_to_qr_code_alphanumeric` method is usually used for encrypting something into a QR code. After ShortCrypt `encrypt` a plaintext, it encodes the cipher into a random-like string based on Base32 format so that it can be inserted into a QR code with the compatibility with alphanumeric mode. 36 | 37 | ```rust 38 | use short_crypt::ShortCrypt; 39 | 40 | let sc = ShortCrypt::new("magickey"); 41 | 42 | assert_eq!("3BHNNR45XZH8PU", sc.encrypt_to_qr_code_alphanumeric("articles")); 43 | assert_eq!("articles".as_bytes().to_vec(), sc.decrypt_qr_code_alphanumeric("3BHNNR45XZH8PU").unwrap()); 44 | ``` 45 | 46 | Besides, in order to reduce the copy times of strings, you can also use `encrypt_to_url_component_and_push_to_string`, `encrypt_to_qr_code_alphanumeric_and_push_to_string` methods to use the same memory space. 47 | 48 | ```rust 49 | use short_crypt::ShortCrypt; 50 | 51 | let sc = ShortCrypt::new("magickey"); 52 | 53 | let url = "https://magiclen.org/".to_string(); 54 | 55 | assert_eq!("https://magiclen.org/2E87Wx52-Tvo", sc.encrypt_to_url_component_and_push_to_string("articles", url)); 56 | 57 | let url = "https://magiclen.org/".to_string(); 58 | 59 | assert_eq!("https://magiclen.org/3BHNNR45XZH8PU", sc.encrypt_to_qr_code_alphanumeric_and_push_to_string("articles", url)); 60 | ``` 61 | 62 | ## Crates.io 63 | 64 | https://crates.io/crates/short-crypt 65 | 66 | ## Documentation 67 | 68 | https://docs.rs/short-crypt 69 | 70 | ## License 71 | 72 | [MIT](LICENSE) -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # array_width = 60 2 | # attr_fn_like_width = 70 3 | binop_separator = "Front" 4 | blank_lines_lower_bound = 0 5 | blank_lines_upper_bound = 1 6 | brace_style = "PreferSameLine" 7 | # chain_width = 60 8 | color = "Auto" 9 | # comment_width = 100 10 | condense_wildcard_suffixes = true 11 | control_brace_style = "AlwaysSameLine" 12 | empty_item_single_line = true 13 | enum_discrim_align_threshold = 80 14 | error_on_line_overflow = false 15 | error_on_unformatted = false 16 | # fn_call_width = 60 17 | fn_params_layout = "Tall" 18 | fn_single_line = false 19 | force_explicit_abi = true 20 | force_multiline_blocks = false 21 | format_code_in_doc_comments = true 22 | doc_comment_code_block_width = 80 23 | format_generated_files = true 24 | format_macro_matchers = true 25 | format_macro_bodies = true 26 | skip_macro_invocations = [] 27 | format_strings = true 28 | hard_tabs = false 29 | hex_literal_case = "Upper" 30 | imports_indent = "Block" 31 | imports_layout = "Mixed" 32 | indent_style = "Block" 33 | inline_attribute_width = 0 34 | match_arm_blocks = true 35 | match_arm_leading_pipes = "Never" 36 | match_block_trailing_comma = true 37 | max_width = 100 38 | merge_derives = true 39 | imports_granularity = "Crate" 40 | newline_style = "Unix" 41 | normalize_comments = false 42 | normalize_doc_attributes = true 43 | overflow_delimited_expr = true 44 | remove_nested_parens = true 45 | reorder_impl_items = true 46 | reorder_imports = true 47 | group_imports = "StdExternalCrate" 48 | reorder_modules = true 49 | short_array_element_width_threshold = 10 50 | # single_line_if_else_max_width = 50 51 | space_after_colon = true 52 | space_before_colon = false 53 | spaces_around_ranges = false 54 | struct_field_align_threshold = 80 55 | struct_lit_single_line = false 56 | # struct_lit_width = 18 57 | # struct_variant_width = 35 58 | tab_spaces = 4 59 | trailing_comma = "Vertical" 60 | trailing_semicolon = true 61 | type_punctuation_density = "Wide" 62 | use_field_init_shorthand = true 63 | use_small_heuristics = "Max" 64 | use_try_shorthand = true 65 | where_single_line = false 66 | wrap_comments = false -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | # ShortCrypt 3 | 4 | ShortCrypt is a very simple encryption library, which aims to encrypt any data into something random at first glance. 5 | Even if these data are similar, the ciphers are still pretty different. 6 | The most important thing is that a cipher is only **4 bits** larger than its plaintext so that it is suitable for data used in a URL or a QR Code. Besides these, it is also an ideal candidate for serial number generation. 7 | 8 | ## Examples 9 | 10 | `encrypt` method can create a `Cipher` tuple separating into a **base** and a **body** of the cipher. The size of a **base** is 4 bits, and the size of a **body** is equal to the plaintext. 11 | 12 | ```rust 13 | extern crate short_crypt; 14 | 15 | use short_crypt::ShortCrypt; 16 | 17 | let sc = ShortCrypt::new("magickey"); 18 | 19 | assert_eq!((8, [216, 78, 214, 199, 157, 190, 78, 250].to_vec()), sc.encrypt("articles")); 20 | assert_eq!("articles".as_bytes().to_vec(), sc.decrypt(&(8, vec![216, 78, 214, 199, 157, 190, 78, 250])).unwrap()); 21 | 22 | ``` 23 | 24 | `encrypt_to_url_component` method is common for encryption in most cases. After ShortCrypt `encrypt` a plaintext, it encodes the cipher into a random-like string based on Base64-URL format so that it can be concatenated with URLs. 25 | 26 | ```rust 27 | extern crate short_crypt; 28 | 29 | use short_crypt::ShortCrypt; 30 | 31 | let sc = ShortCrypt::new("magickey"); 32 | 33 | assert_eq!("2E87Wx52-Tvo", sc.encrypt_to_url_component("articles")); 34 | assert_eq!("articles".as_bytes().to_vec(), sc.decrypt_url_component("2E87Wx52-Tvo").unwrap()); 35 | ``` 36 | 37 | `encrypt_to_qr_code_alphanumeric` method is usually used for encrypting something into a QR code. After ShortCrypt `encrypt` a plaintext, it encodes the cipher into a random-like string based on Base32 format so that it can be inserted into a QR code with the compatibility with alphanumeric mode. 38 | 39 | ```rust 40 | extern crate short_crypt; 41 | 42 | use short_crypt::ShortCrypt; 43 | 44 | let sc = ShortCrypt::new("magickey"); 45 | 46 | assert_eq!("3BHNNR45XZH8PU", sc.encrypt_to_qr_code_alphanumeric("articles")); 47 | assert_eq!("articles".as_bytes().to_vec(), sc.decrypt_qr_code_alphanumeric("3BHNNR45XZH8PU").unwrap()); 48 | ``` 49 | 50 | Besides, in order to reduce the copy times of strings, you can also use `encrypt_to_url_component_and_push_to_string`, `encrypt_to_qr_code_alphanumeric_and_push_to_string` methods to use the same memory space. 51 | 52 | ```rust 53 | extern crate short_crypt; 54 | 55 | use short_crypt::ShortCrypt; 56 | 57 | let sc = ShortCrypt::new("magickey"); 58 | 59 | let url = "https://magiclen.org/".to_string(); 60 | 61 | assert_eq!("https://magiclen.org/2E87Wx52-Tvo", sc.encrypt_to_url_component_and_push_to_string("articles", url)); 62 | 63 | let url = "https://magiclen.org/".to_string(); 64 | 65 | assert_eq!("https://magiclen.org/3BHNNR45XZH8PU", sc.encrypt_to_qr_code_alphanumeric_and_push_to_string("articles", url)); 66 | ``` 67 | */ 68 | 69 | #![no_std] 70 | 71 | #[macro_use] 72 | extern crate alloc; 73 | 74 | pub extern crate base32; 75 | pub extern crate base64_url; 76 | 77 | use alloc::{string::String, vec::Vec}; 78 | use core::fmt::{self, Debug, Formatter}; 79 | 80 | pub use base64_url::base64; 81 | use crc_any::{CRCu64, CRCu8}; 82 | 83 | /// A tuple. The first `u8` value is the **base** which only takes 4 bits. The second `Vec` value is the **body** whose size is equal to the plaintext. You can use your own algorithms to combine them together, or just use `encrypt_to_url_component` or `encrypt_to_qr_code_alphanumeric` to output them as a random-like string. 84 | pub type Cipher = (u8, Vec); 85 | 86 | pub struct ShortCrypt { 87 | hashed_key: [u8; 8], 88 | key_sum_rev: u64, 89 | } 90 | 91 | impl Debug for ShortCrypt { 92 | #[inline] 93 | fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { 94 | debug_helper::impl_debug_for_struct!(ShortCrypt, f, self, let .hashed_key = self.hashed_key.as_ref(), (.key_sum_rev, "{:X}", self.key_sum_rev)); 95 | } 96 | } 97 | 98 | macro_rules! u8_to_string_64 { 99 | ($i:expr) => { 100 | if $i < 10 { 101 | $i + b'0' 102 | } else if (10..36).contains(&$i) { 103 | $i - 10 + b'A' 104 | } else if (36..62).contains(&$i) { 105 | $i - 36 + b'a' 106 | } else if $i == 62 { 107 | b'-' 108 | } else { 109 | b'_' 110 | } 111 | }; 112 | } 113 | 114 | macro_rules! string_64_to_u8 { 115 | ($c:expr) => { 116 | if $c >= b'0' && $c <= b'9' { 117 | $c - b'0' 118 | } else if $c >= b'A' && $c <= b'Z' { 119 | $c + 10 - b'A' 120 | } else if $c >= b'a' && $c <= b'z' { 121 | $c + 36 - b'a' 122 | } else if $c == b'-' { 123 | 62 124 | } else { 125 | 63 126 | } 127 | }; 128 | } 129 | 130 | macro_rules! u8_to_string_32 { 131 | ($i:expr) => { 132 | if $i < 10 { 133 | $i + b'0' 134 | } else { 135 | $i - 10 + b'A' 136 | } 137 | }; 138 | } 139 | 140 | macro_rules! string_32_to_u8 { 141 | ($c:expr) => { 142 | if $c >= b'0' && $c <= b'9' { 143 | $c - b'0' 144 | } else { 145 | $c + 10 - b'A' 146 | } 147 | }; 148 | } 149 | 150 | impl ShortCrypt { 151 | /// Create a new ShortCrypt instance. 152 | pub fn new>(key: S) -> ShortCrypt { 153 | let key_bytes = key.as_ref().as_bytes(); 154 | 155 | let hashed_key = { 156 | let mut hasher = CRCu64::crc64we(); 157 | 158 | hasher.digest(key_bytes); 159 | 160 | hasher.get_crc().to_be_bytes() 161 | }; 162 | 163 | let mut key_sum = 0u64; 164 | 165 | for n in key_bytes.iter().copied() { 166 | key_sum = key_sum.wrapping_add(u64::from(n)); 167 | } 168 | 169 | let key_sum_rev = key_sum.reverse_bits(); 170 | 171 | ShortCrypt { 172 | hashed_key, 173 | key_sum_rev, 174 | } 175 | } 176 | 177 | pub fn encrypt>(&self, plaintext: &T) -> Cipher { 178 | let data = plaintext.as_ref(); 179 | 180 | let len = data.len(); 181 | 182 | let hashed_value = { 183 | let mut crc8 = CRCu8::crc8cdma2000(); 184 | 185 | crc8.digest(data); 186 | crc8.get_crc() 187 | }; 188 | 189 | let base = hashed_value % 32; 190 | 191 | let mut encrypted = Vec::with_capacity(len); 192 | 193 | let mut m = base; 194 | let mut sum = u64::from(base); 195 | 196 | for (i, d) in data.iter().enumerate() { 197 | let offset = self.hashed_key[i % 8] ^ base; 198 | 199 | let v = d ^ offset; 200 | 201 | encrypted.push(v); 202 | 203 | m ^= v; 204 | sum = sum.wrapping_add(u64::from(v)); 205 | } 206 | 207 | let sum: [u8; 8] = sum.to_be_bytes(); 208 | 209 | let hashed_array: [u8; 8] = { 210 | let mut hasher = CRCu64::crc64we(); 211 | 212 | hasher.digest(&[m]); 213 | hasher.digest(&sum); 214 | 215 | hasher.get_crc().to_be_bytes() 216 | }; 217 | 218 | let mut path = Vec::with_capacity(len); 219 | 220 | for i in 0..len { 221 | let index = i % 8; 222 | path.push((hashed_array[index] ^ self.hashed_key[index]) as usize % len); 223 | } 224 | 225 | for (i, p) in path.iter().copied().enumerate() { 226 | if i == p { 227 | continue; 228 | } 229 | 230 | encrypted.swap(i, p); 231 | } 232 | 233 | (base, encrypted) 234 | } 235 | 236 | pub fn decrypt(&self, data: &Cipher) -> Result, &'static str> { 237 | let base = data.0; 238 | let data = &data.1; 239 | 240 | if base > 31 { 241 | return Err("The base is not correct."); 242 | } 243 | 244 | let len = data.len(); 245 | 246 | let mut decrypted = Vec::with_capacity(len); 247 | 248 | self.decrypt_inner(base, data, &mut decrypted); 249 | 250 | Ok(decrypted) 251 | } 252 | 253 | fn decrypt_inner(&self, base: u8, data: &[u8], output: &mut Vec) { 254 | let len = data.len(); 255 | 256 | let mut m = base; 257 | let mut sum = u64::from(base); 258 | 259 | for v in data.iter().copied() { 260 | m ^= v; 261 | sum = sum.wrapping_add(u64::from(v)); 262 | } 263 | 264 | let sum: [u8; 8] = sum.to_be_bytes(); 265 | 266 | let hashed_array: [u8; 8] = { 267 | let mut hasher = CRCu64::crc64we(); 268 | 269 | hasher.digest(&[m]); 270 | hasher.digest(&sum); 271 | 272 | hasher.get_crc().to_be_bytes() 273 | }; 274 | 275 | let mut path = Vec::with_capacity(len); 276 | 277 | for i in 0..len { 278 | let index = i % 8; 279 | path.push((hashed_array[index] ^ self.hashed_key[index]) as usize % len); 280 | } 281 | 282 | let mut data = data.to_vec(); 283 | 284 | for (i, p) in path.iter().copied().enumerate().rev() { 285 | if i == p { 286 | continue; 287 | } 288 | 289 | data.swap(i, p); 290 | } 291 | 292 | for (i, d) in data.iter().enumerate() { 293 | let offset = self.hashed_key[i % 8] ^ base; 294 | 295 | output.push(d ^ offset); 296 | } 297 | } 298 | 299 | pub fn encrypt_to_url_component>(&self, data: &T) -> String { 300 | let (base, encrypted) = self.encrypt(data); 301 | 302 | let base = u8_to_string_64!(base); 303 | 304 | let base_char = base as char; 305 | 306 | let mut result = String::with_capacity(1 + ((encrypted.len() * 4 + 2) / 3)); 307 | 308 | base64_url::encode_to_string(&encrypted, &mut result); 309 | 310 | let mut sum = u64::from(base); 311 | 312 | for n in result.bytes() { 313 | sum = sum.wrapping_add(u64::from(n)); 314 | } 315 | 316 | let base_index = ((self.key_sum_rev ^ sum) % ((result.len() + 1) as u64)) as usize; 317 | 318 | result.insert(base_index, base_char); 319 | 320 | result 321 | } 322 | 323 | pub fn encrypt_to_url_component_and_push_to_string, S: Into>( 324 | &self, 325 | data: &T, 326 | output: S, 327 | ) -> String { 328 | let (base, encrypted) = self.encrypt(data); 329 | 330 | let base = u8_to_string_64!(base); 331 | 332 | let base_char = base as char; 333 | 334 | let mut output = output.into(); 335 | 336 | let original_len = output.len(); 337 | 338 | base64_url::encode_to_string(&encrypted, &mut output); 339 | 340 | let mut sum = u64::from(base); 341 | 342 | for n in output.bytes().skip(original_len) { 343 | sum = sum.wrapping_add(u64::from(n)); 344 | } 345 | 346 | let base_index = 347 | ((self.key_sum_rev ^ sum) % ((output.len() - original_len + 1) as u64)) as usize; 348 | 349 | output.insert(original_len + base_index, base_char); 350 | 351 | output 352 | } 353 | 354 | pub fn decrypt_url_component>( 355 | &self, 356 | url_component: S, 357 | ) -> Result, &'static str> { 358 | let bytes = url_component.as_ref().as_bytes(); 359 | let len = bytes.len(); 360 | 361 | if len < 1 { 362 | return Err("The URL component is incorrect."); 363 | } 364 | 365 | let base_index = { 366 | let mut sum = 0u64; 367 | 368 | for n in bytes.iter().copied() { 369 | sum = sum.wrapping_add(u64::from(n)); 370 | } 371 | 372 | ((self.key_sum_rev ^ sum) % (len as u64)) as usize 373 | }; 374 | 375 | let base = string_64_to_u8!(bytes[base_index]); 376 | 377 | if base > 31 { 378 | return Err("The URL component is incorrect."); 379 | } 380 | 381 | let encrypted_base64_url = [&bytes[..base_index], &bytes[(base_index + 1)..]].concat(); 382 | 383 | let encrypted = base64_url::decode(&encrypted_base64_url) 384 | .map_err(|_| "The URL component is incorrect.")?; 385 | 386 | self.decrypt(&(base, encrypted)) 387 | } 388 | 389 | pub fn decrypt_url_component_and_push_to_vec>( 390 | &self, 391 | url_component: S, 392 | mut output: Vec, 393 | ) -> Result, &'static str> { 394 | let bytes = url_component.as_ref().as_bytes(); 395 | let len = bytes.len(); 396 | 397 | if len < 1 { 398 | return Err("The URL component is incorrect."); 399 | } 400 | 401 | let base_index = { 402 | let mut sum = 0u64; 403 | 404 | for n in bytes.iter().copied() { 405 | sum = sum.wrapping_add(u64::from(n)); 406 | } 407 | 408 | ((self.key_sum_rev ^ sum) % (len as u64)) as usize 409 | }; 410 | 411 | let base = string_64_to_u8!(bytes[base_index]); 412 | 413 | if base > 31 { 414 | return Err("The URL component is incorrect."); 415 | } 416 | 417 | let encrypted_base64_url = [&bytes[..base_index], &bytes[(base_index + 1)..]].concat(); 418 | 419 | let encrypted = base64_url::decode(&encrypted_base64_url) 420 | .map_err(|_| "The URL component is incorrect.")?; 421 | 422 | let len = encrypted.len(); 423 | 424 | output.reserve(len); 425 | 426 | self.decrypt_inner(base, &encrypted, &mut output); 427 | 428 | Ok(output) 429 | } 430 | 431 | pub fn encrypt_to_qr_code_alphanumeric>(&self, data: &T) -> String { 432 | let (base, encrypted) = self.encrypt(data); 433 | 434 | let base = u8_to_string_32!(base); 435 | 436 | let base_char = base as char; 437 | 438 | let mut result = String::with_capacity(1 + ((encrypted.len() * 8 + 4) / 5)); 439 | 440 | result.push_str(&base32::encode( 441 | base32::Alphabet::RFC4648 { 442 | padding: false 443 | }, 444 | &encrypted, 445 | )); 446 | 447 | let mut sum = u64::from(base); 448 | 449 | for n in result.bytes() { 450 | sum = sum.wrapping_add(u64::from(n)); 451 | } 452 | 453 | let base_index = ((self.key_sum_rev ^ sum) % ((result.len() + 1) as u64)) as usize; 454 | 455 | result.insert(base_index, base_char); 456 | 457 | result 458 | } 459 | 460 | pub fn encrypt_to_qr_code_alphanumeric_and_push_to_string< 461 | T: ?Sized + AsRef<[u8]>, 462 | S: Into, 463 | >( 464 | &self, 465 | data: &T, 466 | output: S, 467 | ) -> String { 468 | let (base, encrypted) = self.encrypt(data); 469 | 470 | let base = u8_to_string_32!(base); 471 | 472 | let base_char = base as char; 473 | 474 | let mut output = output.into(); 475 | 476 | let original_len = output.len(); 477 | 478 | output.push_str(&base32::encode( 479 | base32::Alphabet::RFC4648 { 480 | padding: false 481 | }, 482 | &encrypted, 483 | )); 484 | 485 | let mut sum = u64::from(base); 486 | 487 | for n in output.bytes().skip(original_len) { 488 | sum = sum.wrapping_add(u64::from(n)); 489 | } 490 | 491 | let base_index = 492 | ((self.key_sum_rev ^ sum) % ((output.len() - original_len + 1) as u64)) as usize; 493 | 494 | output.insert(original_len + base_index, base_char); 495 | 496 | output 497 | } 498 | 499 | pub fn decrypt_qr_code_alphanumeric>( 500 | &self, 501 | qr_code_alphanumeric: S, 502 | ) -> Result, &'static str> { 503 | let bytes = qr_code_alphanumeric.as_ref().as_bytes(); 504 | let len = bytes.len(); 505 | 506 | if len < 1 { 507 | return Err("The QR code alphanumeric text is incorrect."); 508 | } 509 | 510 | let base_index = { 511 | let mut sum = 0u64; 512 | 513 | for n in bytes.iter().copied() { 514 | sum = sum.wrapping_add(u64::from(n)); 515 | } 516 | 517 | ((self.key_sum_rev ^ sum) % (len as u64)) as usize 518 | }; 519 | 520 | let base = string_32_to_u8!(bytes[base_index]); 521 | 522 | if base > 31 { 523 | return Err("The QR code alphanumeric text is incorrect."); 524 | } 525 | 526 | let encrypted_base32 = 527 | String::from_utf8([&bytes[..base_index], &bytes[(base_index + 1)..]].concat()) 528 | .map_err(|_| "The QR code alphanumeric text is incorrect.")?; 529 | 530 | let encrypted = match base32::decode( 531 | base32::Alphabet::RFC4648 { 532 | padding: false 533 | }, 534 | &encrypted_base32, 535 | ) { 536 | Some(t) => t, 537 | None => return Err("The QR code alphanumeric text is incorrect."), 538 | }; 539 | 540 | self.decrypt(&(base, encrypted)) 541 | } 542 | 543 | pub fn decrypt_qr_code_alphanumeric_and_push_to_vec>( 544 | &self, 545 | qr_code_alphanumeric: S, 546 | mut output: Vec, 547 | ) -> Result, &'static str> { 548 | let bytes = qr_code_alphanumeric.as_ref().as_bytes(); 549 | let len = bytes.len(); 550 | 551 | if len < 1 { 552 | return Err("The QR code alphanumeric text is incorrect."); 553 | } 554 | 555 | let base_index = { 556 | let mut sum = 0u64; 557 | 558 | for n in bytes.iter().copied() { 559 | sum = sum.wrapping_add(u64::from(n)); 560 | } 561 | 562 | ((self.key_sum_rev ^ sum) % (len as u64)) as usize 563 | }; 564 | 565 | let base = string_32_to_u8!(bytes[base_index]); 566 | 567 | if base > 31 { 568 | return Err("The QR code alphanumeric text is incorrect."); 569 | } 570 | 571 | let encrypted_base32 = 572 | String::from_utf8([&bytes[..base_index], &bytes[(base_index + 1)..]].concat()) 573 | .map_err(|_| "The QR code alphanumeric text is incorrect.")?; 574 | 575 | let encrypted = match base32::decode( 576 | base32::Alphabet::RFC4648 { 577 | padding: false 578 | }, 579 | &encrypted_base32, 580 | ) { 581 | Some(t) => t, 582 | None => return Err("The QR code alphanumeric text is incorrect."), 583 | }; 584 | 585 | let len = encrypted.len(); 586 | 587 | output.reserve(len); 588 | 589 | self.decrypt_inner(base, &encrypted, &mut output); 590 | 591 | Ok(output) 592 | } 593 | } 594 | -------------------------------------------------------------------------------- /tests/tests.rs: -------------------------------------------------------------------------------- 1 | use short_crypt::ShortCrypt; 2 | 3 | #[test] 4 | fn test_encrypt() { 5 | let sc = ShortCrypt::new("magickey"); 6 | 7 | assert_eq!((8, [216, 78, 214, 199, 157, 190, 78, 250].to_vec()), sc.encrypt("articles")); 8 | } 9 | 10 | #[test] 11 | fn test_decrypt() { 12 | let sc = ShortCrypt::new("magickey"); 13 | 14 | assert_eq!( 15 | b"articles".to_vec(), 16 | sc.decrypt(&(8, vec![216, 78, 214, 199, 157, 190, 78, 250])).unwrap() 17 | ); 18 | } 19 | 20 | #[test] 21 | fn test_encrypt_decrypt() { 22 | let sc = ShortCrypt::new("magickey"); 23 | 24 | let data = b"articles"; 25 | 26 | assert_eq!(data.to_vec(), sc.decrypt(&sc.encrypt(data)).unwrap()); 27 | } 28 | 29 | #[test] 30 | fn test_encrypt_to_url_component() { 31 | let sc = ShortCrypt::new("magickey"); 32 | 33 | assert_eq!("2E87Wx52-Tvo", sc.encrypt_to_url_component("articles")); 34 | } 35 | 36 | #[test] 37 | fn test_encrypt_to_url_component_and_push_to_string() { 38 | let url = "https://magiclen.org/".to_string(); 39 | 40 | let sc = ShortCrypt::new("magickey"); 41 | 42 | assert_eq!( 43 | "https://magiclen.org/2E87Wx52-Tvo", 44 | sc.encrypt_to_url_component_and_push_to_string("articles", url) 45 | ); 46 | } 47 | 48 | #[test] 49 | fn test_decrypt_url_component() { 50 | let sc = ShortCrypt::new("magickey"); 51 | 52 | assert_eq!(b"articles".to_vec(), sc.decrypt_url_component("2E87Wx52-Tvo").unwrap()); 53 | } 54 | 55 | #[test] 56 | fn test_decrypt_url_component_and_push_to_vec() { 57 | let url = b"https://magiclen.org/".to_vec(); 58 | 59 | let sc = ShortCrypt::new("magickey"); 60 | 61 | assert_eq!( 62 | b"https://magiclen.org/articles".to_vec(), 63 | sc.decrypt_url_component_and_push_to_vec("2E87Wx52-Tvo", url).unwrap() 64 | ); 65 | } 66 | 67 | #[test] 68 | fn test_encrypt_to_qr_code_alphanumeric() { 69 | let sc = ShortCrypt::new("magickey"); 70 | 71 | assert_eq!("3BHNNR45XZH8PU", sc.encrypt_to_qr_code_alphanumeric("articles")); 72 | } 73 | 74 | #[test] 75 | fn test_encrypt_to_qr_code_alphanumeric_and_push_to_string() { 76 | let url = "https://magiclen.org/".to_string(); 77 | 78 | let sc = ShortCrypt::new("magickey"); 79 | 80 | assert_eq!( 81 | "https://magiclen.org/3BHNNR45XZH8PU", 82 | sc.encrypt_to_qr_code_alphanumeric_and_push_to_string("articles", url) 83 | ); 84 | } 85 | 86 | #[test] 87 | fn test_decrypt_qr_code_alphanumeric() { 88 | let sc = ShortCrypt::new("magickey"); 89 | 90 | assert_eq!(b"articles".to_vec(), sc.decrypt_qr_code_alphanumeric("3BHNNR45XZH8PU").unwrap()); 91 | } 92 | 93 | #[test] 94 | fn test_decrypt_qr_code_alphanumeric_and_push_to_vec() { 95 | let url = b"https://magiclen.org/".to_vec(); 96 | 97 | let sc = ShortCrypt::new("magickey"); 98 | 99 | assert_eq!( 100 | b"https://magiclen.org/articles".to_vec(), 101 | sc.decrypt_qr_code_alphanumeric_and_push_to_vec("3BHNNR45XZH8PU", url).unwrap() 102 | ); 103 | } 104 | --------------------------------------------------------------------------------