Packages and Cargo
Now that we know how to create a simple program in Rust, let's explore Cargo, the Rust package manager. Cargo is a command-line application that manages your application dependencies and compiles your code.
Rust has a community package registry at https://crates.io. You can use that website to search for a library that you can use in your application. Don't forget to check the license of the library or application that you want to use. If you register on that website, you can use Cargo to publicly distribute your library or binary.
How do we install Cargo into our system? The good news is Cargo is already installed if you install the Rust toolchain in the stable channel using rustup
.
Cargo package layout
Let's try using Cargo in our application. First, let's copy the application that we wrote earlier:
cp -r 02ComplexProgram 03Packages cd 03Packages cargo init . --name our_package
Since we already have an existing application, we can initialize our existing application with cargo init
. Notice we add the --name
option because we are prefixing our folder name with a number, and a Rust package name cannot start with a number.
If we are creating a new application, we can use the cargo new package_name
command. To create a library-only package instead of a binary package, you can pass the --lib
option to cargo new
.
You will see two new files, Cargo.toml
and Cargo.lock
, inside the folder. The .toml
file is a file format commonly used as a configuration file. The lock
file is generated automatically by Cargo, and we don't usually change the content manually. It's also common to add Cargo.lock
to your source code versioning application ignore list, such as .gitignore
, for example.
Let's check the content of the Cargo.toml
file:
[package]
name = "our_package"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at
https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
[[bin]]
name = "our_package"
path = "main.rs"
As you can see, we can define basic things for our application such as name
and version
. We can also add important information such as authors, homepage, repository, and much more. We can also add dependencies that we want to use in the Cargo application.
One thing that stands out is the edition configuration. The Rust edition is an optional marker to group various Rust language releases that have the same compatibility. When Rust 1.0 was released, the compiler did not have the capability to know the async
and await
keywords. After async
and await
were added, it created all sorts of problems with older compilers. The solution to that problem was to introduce Rust editions. Three editions have been defined: 2015, 2018, and 2021.
Right now, the Rust compiler can compile our package perfectly fine, but it is not very idiomatic because a Cargo project has conventions on file and folder names and structures. Let's change the files and directory structure a little bit:
- A package is expected to reside in the
src
directory. Let's change theCargo.toml
file[[bin]]
path from"main.rs"
to"src/main.rs"
. - Create the
src
directory inside our application folder. Then, move themain.rs
file and theencryptor
folder to thesrc
folder. - Add these lines to
Cargo.toml
after[[bin]]
:[lib] name = "our_package" path = "src/lib.rs"
- Let's create the
src/lib.rs
file and move this line fromsrc/main.rs
tosrc/lib.rs
:pub mod encryptor;
- We can then simplify using both the
rot13
andEncryptable
modules in ourmain.rs
file:use our_package::encryptor::{rot13, Encryptable}; use std::io; fn main() { ... println!( "Your encrypted string: {}", rot13::Rot13(user_input).encrypt() ); }
- We can check whether there's an error that prevents the code from being compiled by typing
cargo check
in the command line. It should produce something like this:> cargo check Checking our_package v0.1.0 (/Users/karuna/Chapter01/03Packages) Finished dev [unoptimized + debuginfo] target(s) in 1.01s
- After that, we can build the binary using the
cargo build
command. Since we didn't specify any option in our command, the default binary should be unoptimized and contain debugging symbols. The default location for the generated binary is in thetarget
folder at the root of the workspace:$ cargo build Compiling our_package v0.1.0 (/Users/karuna/Chapter01/03Packages) Finished dev [unoptimized + debuginfo] target(s) in 5.09s
You can then run the binary in the target
folder as follows:
./target/debug/our_package
debug
is enabled by the default dev profile, and our_package
is the name that we specify in Cargo.toml
.
If you want to create a release binary, you can specify the --release
option, cargo build --release
. You can find the release binary in ./target/release/our_package
.
You can also type cargo run
, which will compile and run the application for you.
Now that we have arranged our application structure, let's add real-world encryption to our application by using a third-party crate.
Using third-party crates
Before we implement another encryptor using a third-party module, let's modify our application a little bit. Copy the previous 03Packages
folder to the new folder, 04Crates
, and use the folder for the following steps:
- We will rename our Encryptor trait as a Cipher trait and modify the functions. The reason is that we only need to think about the output of the type, not the encrypt process itself:
- Let's change the content of
src/lib.rs
topub mod cipher;
. - After that, rename the
encryptor
folder ascipher
. - Then, modify the Encryptable trait into the following:
pub trait Cipher { fn original_string(&self) -> String; fn encrypted_string(&self) -> String; }
- Let's change the content of
The reality is we only need functions to show the original string and the encrypted string. We don't need to expose the encryption in the type itself.
- After that, let's also change
src/cipher/rot13.rs
to use the renamed trait:impl super::Cipher for Rot13 { fn original_string(&self) -> String { String::from(&self.0) } fn encrypted_string(&self) -> String { self.0 .chars() .map(|ch| match ch { 'a'..='m' | 'A'..='M' => (ch as u8 + 13) as char, 'n'..='z' | 'N'..='Z' => (ch as u8 – 13) as char, _ => ch, }) .collect() } }
- Let's also modify
main.rs
to use the new trait and function:use our_package::cipher::{rot13, Cipher}; … fn main() { … println!( "Your encrypted string: {}", rot13::Rot13(user_input).encrypted_string() ); }
The next step is to determine what encryption and library we want to use for our new type. We can go to https://crates.io and search for an available crate. After searching for a real-world encryption algorithm on the website, we found https://crates.io/crates/rsa. We found that the RSA algorithm is a secure algorithm, the crate has good documentation and has been audited by security researchers, the license is compatible with what we need, and there's a huge number of downloads. Aside from checking the source code of this library, all indications show that this is a good crate to use. Luckily, there's an install section on the right side of that page. Besides the rsa
crate, we are also going to use the rand
crate, since the RSA algorithm requires a random number generator. Since the generated encryption is in bytes, we must encode it somehow to string
. One of the common ways is to use base64
.
- Add these lines in our
Cargo.toml
file, under the[dependencies]
section:rsa = "0.5.0" rand = "0.8.4" base64 = "0.13.0"
- The next step should be adding a new module and typing using the
rsa
crate. But, for this type, we want to modify it a little bit. First, we want to create an associated function, which might be called a constructor in other languages. We want to then encrypt the input string in this function and store the encrypted string in a field. There's a saying that all data not in processing should be encrypted by default, but the fact is that we as programmers rarely do this.
Since RSA encryption is dealing with byte manipulation, there's a possibility of errors, so the return value of the associated function should be wrapped in the Result
type. There's no compiler rule, but if a function cannot fail, the return should be straightforward. Regardless of whether or not a function can produce a result, the return
value should be Option
, but if a function can produce an error, it's better to use Result
.
The encrypted_string()
method should return the stored encrypted string, and the original_string()
method should decrypt the stored string and return the plain text.
In src/cipher/mod.rs
, change the code to the following:
pub trait Cipher { fn original_string(&self) -> Result<String, Box<dyn Error>>; fn encrypted_string(&self) -> Result<String, Box<dyn Error>>; }
- Since we changed the definition of the trait, we have to change the code in
src/cipher/rot13.rs
as well. Change the code to the following:use std::error::Error; pub struct Rot13(pub String); impl super::Cipher for Rot13 { fn original_string(&self) -> Result<String, Box<dyn Error>> { Ok(String::from(&self.0)) } fn encrypted_string(&self) -> Result<String, Box<dyn Error>> { Ok(self .0 ... .collect()) } }
- Let's add the following line in the
src/cipher/mod.rs
file:pub mod rsa;
- After that, create
rsa.rs
inside thecipher
folder and create theRsa
struct inside it. Notice that we useRsa
instead ofRSA
as the type name. The convention is to useCamelCase
for type:use std::error::Error; pub struct Rsa { data: String, } impl Rsa { pub fn new(input: String) -> Result<Self, Box< dyn Error>> { unimplemented!(); } } impl super::Cipher for Rsa { fn original_string(&self) -> Result<String, ()> { unimplemented!(); } fn encrypted_string(&self) -> Result<String, ()> { Ok(String::from(&self.data)) } }
There are a couple of things we can observe. The first one is the data
field does not have the pub
keyword since we want to make it private. You can see that we have two impl
blocks: one is for defining the methods of the Rsa
type itself, and the other is for implementing the Cipher
trait.
Also, the new()
function does not have self
, mut self
, &self
, or &mut self
as the first parameter. Consider it as a static method in other languages. This method is returning Result
, which is either Ok(Self)
or Box<dyn Error>
. The Self
instance is the instance of the Rsa
struct, but we'll discuss Box<dyn Error>
later when we talk about error handling in Chapter 7, Handling Errors in Rust and Rocket. Right now, we haven't implemented this method, hence the usage of the unimplemented!()
macro. Macros in Rust look like a function but with an extra bang (!).
- Now, let's implement the associated function. Modify
src/cipher/rsa.rs
:use rand::rngs::OsRng; use rsa::{PaddingScheme, PublicKey, RsaPrivateKey}; use std::error::Error; const KEY_SIZE: usize = 2048; pub struct Rsa { data: String, private_key: RsaPrivateKey, } impl Rsa { pub fn new(input: String) -> Result<Self, Box< dyn Error>> { let mut rng = OsRng; let private_key = RsaPrivateKey::new(&mut rng, KEY_SIZE)?; let public_key = private_key.to_public_key(); let input_bytes = input.as_bytes(); let encrypted_data = public_key.encrypt(&mut rng, PaddingScheme ::new_pkcs1v15_encrypt(), input_bytes)?; let encoded_data = base64::encode(encrypted_data); Ok(Self { data: encoded_data, private_key, }) } }
The first thing we do is declare the various types we are going to use. After that, we define a constant to denote what size key we are going to use.
If you understand the RSA algorithm, you already know that it's an asymmetric algorithm, meaning we have two keys: a public key and a private key. We use the public key to encrypt data and use the private key to decrypt the data. We can generate and give the public key to the other party, but we don't want to give the private key to the other party. That means we must store the private key inside the struct as well.
The new()
implementation is pretty straightforward. The first thing we do is declare a random number generator, rng
. We then generate the RSA private key. But, pay attention to the question mark operator (?
) on the initialization of the private key. If a function returns Result
, we can quickly return the error generated by calling any method or function inside it by using (?
) after that function.
Then, we generate the RSA public key from a private key, encode the input string as bytes, and encrypt the data. Since encrypting the data might have resulted in an error, we use the question mark operator again. We then encode the encrypted bytes as a base64
string and initialize Self
, which means the Rsa
struct itself.
- Now, let's implement the
original_string()
method. We should do the opposite of what we do when we create the struct:fn original_string(&self) -> Result<String, Box<dyn Error>> { let decoded_data = base64::decode(&self.data)?; let decrypted_data = self .private_key .decrypt(PaddingScheme:: new_pkcs1v15_encrypt(), &decoded_data)?; Ok(String::from_utf8(decrypted_data)?) }
First, we decode the base64
encoded string in the data
field. Then, we decrypt the decoded bytes and convert them back to a string.
- Now that we have finished our
Rsa
type, let's use it in ourmain.rs
file:fn main() { ... println!( "Your encrypted string: {}", rot13::Rot13(user_input).encrypted_ string().unwrap() ); println!("Input the string you want to encrypt:"); let mut user_input = String::new(); io::stdin() .read_line(&mut user_input) .expect("Cannot read input"); let encrypted_input = rsa::Rsa::new( user_input).expect(""); let encrypted_string = encrypted_input.encrypted_ string().expect(""); println!("Your encrypted string: {}", encrypted_string); let decrypted_string = encrypted_input .original_string().expect(""); println!("Your original string: {}", decrypted_string); }
Some of you might wonder why we redeclared the user_input
variable. The simple explanation is that Rust already moved the resource to the new Rot13
type, and Rust does not allow the reuse of the moved value. You can try commenting on the second variable declaration and compile the application to see the explanation. We will discuss the Rust borrow checker and moving in more detail in Chapter 9, Displaying Users' Post.
Now, try running the program by typing cargo run
:
$ cargo run Compiling cfg-if v1.0.0 Compiling subtle v2.4.1 Compiling const-oid v0.6.0 Compiling ppv-lite86 v0.2.10 ... Compiling our_package v0.1.0 (/Users/karuna//Chapter01/04Crates) Finished dev [unoptimized + debuginfo] target(s) in 3.17s Running `target/debug/our_package` Input the string you want to encrypt: first Your encrypted string: svefg Input the string you want to encrypt: second Your encrypted string: lhhb9RvG9zI75U2VC3FxvfUujw0cVqqZFgPXhNixQTF7RoVBEJh2inn7sEefDB7eNlQcf09lD2nULfgc2mK55ZE+UUcYzbMDu45oTaPiDPog4L6FRVpbQR27bkOj9Bq1KS+QAvRtxtTbTa1L5/OigZbqBc2QOm2yHLCimMPeZKhLBtK2whhtzIDM8l5AYTBg+rA688ZfB7ZI4FSRm4/h22kNzSPo1DECI04ZBprAq4hWHxEKRwtn5TkRLhClGFLSYKkY7Ajjr3EOf4QfkUvFFhZ0qRDndPI5c9RecavofVLxECrYfv5ygYRmW3B1cJn4vcBhVKfQF0JQ+vs+FuTUpw== Your original string: second
You will see that Cargo automatically downloaded the dependencies and builds them one by one. Also, you might notice that encrypting using the Rsa
type took a while. Isn't Rust supposed to be a fast system language? The RSA algorithm itself is a slow algorithm, but that's not the real cause of the slowness. Because we are running the program in a development profile, the Rust compiler generates an application binary with all the debugging information and does not optimize the resulting binary. On the other hand, if you build the application using the --release
flag, the compiler generates an optimized application binary and strips the debugging symbols. The resulting binary compiled with the release flag should execute faster than the debug binary. Try doing it yourself so you'll remember how to build a release binary.
In this section, we have learned about Cargo and third-party packages, so next, let's find out where to find help and documentation for the tools that we have used.