diff --git a/README.md b/README.md index 20033f2..0380c42 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ It uses amazing [sequoia-pgp](https://sequoia-pgp.org/) library for the actual O ### Build dependencies in Fedora ``` -sudo dnf install nettle clang clang-devel +sudo dnf install nettle clang clang-devel nettle-dev ``` @@ -27,84 +27,17 @@ maturin develop ```Python >>> import johnnycanencrypt as jce ->>> j = jce.Johnny("secret.asc") +>>> j = jce.Johnny("public.asc") >>> data = j.encrypt_bytes(b"kushal \xf0\x9f\x90\x8d") ->>> print(data) ------BEGIN PGP MESSAGE----- - -wcFMAwhsWpR1vDokAQ//UQGjrmmPLP0Td8pELf8XZEPh6fY9Xad6XHH6vQjGwvjG -36kK8ejRqyLbZpwVOO1FUfiZt6AyaeIEeEagoolMxmFl67mWBHsw5Z2NUPhydAwJ -EX+VdFn6CtRzQ0xG3T7rOCrsR50COO13gc4fIAn7Rxj1DyjqlFvur10FNnxRm0iJ -jnOwPnWVWKwoROzevfQd1Oef0n4nbkDUuyrS9oHSRFhFF/9I9bGtJhho0VIIcmFG -YVkhR0+QROTZ4edKotUg0R3UUfHmfwT0XcybGWMG/8Nh3W8pYuxwDdtbSMNDZzxu -o9TdpLrgoRIkhyGmuYWrURrRN1hmce5B6XOagWu7aKL7pFhP7Vd6LLoliDwY4G6x -1yKHbSo/1FEof7WBDujCksuVedUO8Gs9giitR/p/U9PBadeyiW0CKTYiTURkkNiF -g79lVfbmM54eZ9zmU+PraVNpekYXvH3o+lvXd5T39mo4Y/dv2fDCjo2aiZ/bE56Q -yn/0Hhmj2ikrsUk3NcuhO4zxt+VLctJt+lfk+R2hal+6NTaRkREdprPp4ltAt/bm -8xwBmqp+FDdxGgY+ItJkG69vsIf4WpPsvBI37fVbeYqrPsaz9NGlz2QKdfQvaH7j -R7rgxf24H2FjbbyNuHF3tJJa4Kfpnhq4nkxA/EdRP9JcVm/X568jLayTLyJGmrbS -PAHlVMLSBXQDApkY+5Veu3teRR5M2BLPr7X/gfeNnTlbZ4kF5S+E+0bjTjrz+6oo -dcsnTYxmcAm9hdPjng== -=1IYb ------END PGP MESSAGE----- - - - ->>> result = j.decrypt_bytes(data.encode("utf-8"), "mysecretpassword") +>>> js = jce.Johnny("secret.asc") +>>> result = js.decrypt_bytes(data, "mysecretpassword") >>> print(result.decode("utf-8")) kushal 🐍 ``` -## Quick API documentation - -Remember that this will change a lot in the coming days. - - -```Python -import johnnycanencrypt as jce -``` - -To create new RSA4096 size key, call `jce.newkey("password", "userid")`, both *password* and *userid* are Python str. -Remember to save them into different files ending with *.asc*. - -To do any encryption/decryption we have to create an object of the **Johnny** class with the private or public key file. -Remember, except **password** input, every else takes `bytes` as input type. - -### Signing a file with detached signature - -```Python -j = Johnny("private.asc") -signature = j.sign_file_detached(b"filename.txt", "password") -with open(b"filename.txt.asc", "wb") as f: - f.write(signature) -``` - -### Verifying a signature - - -```Python -j = Johnny("public.asc") -with open(b"filename.txt.asc", "b") as f: - sig = f.read() - -verified = j.verify_file(b"filename.txt", sig) -print(f"Verified: {verified}") -``` - -For signing and verifying there are similar method available for bytes, `verify_bytes`, `sign_bytes_detached`. - - -### Encrypting and decrypting files - -```Python -j = jce.Johnny("public.asc") -assert j.encrypt_file(inputfile, output_file_path) -jp = jce.Johnny("secret.asc") - -result = jp.decrypt_file(output_file_path, decrypted_output_path, "password") -``` +## API documentation -Note that, in this context, `inputfile`, `output_file_path`, and `decrypted_output_path` should be binary, not strings. +Please go through the [full API documentation](https://johnnycanencrypt.readthedocs.io/en/latest/) for detailed descriptions. ## LICENSE: GPLv3+ diff --git a/changelog.md b/changelog.md index a10cc77..f43141b 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,9 @@ - If the public/secret key file is missing, while trying to create a `Johnny` object will raise `FileNotFound` error. - If one tries to decrypt using a public key file, it will throw `AttributeError`. +- `encrypt_bytes` now returns bytes (instead of string). +- `encrypt_bytes` takes a third argument, `armor` as boolean, to return ascii-armored bytes or not. +- `encrypt_file` takes a third argument, `armor` as boolean, writes the output file ascii armored if true. ## [0.1.0] - 2020-07-11 diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..d466007 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,92 @@ +API Documentation +================== + +For the rest of the documentation we assume that you imported the module as following. + +:: + + + >>> import johnnycanencrypt as jce + + +.. function:: newkey(password, userid) + + Use the `newkey` function in the module to create a new keypair. It takes two arguments as str, a password, and userid. + By default it creates the key with RSA4096, and returns a tuple of public,secret key as str. Raises `FileNotFound` error + if the key file can not be accessed. + + :: + + >>> public, secret = jce.newkey("my super secret password using diceware", "test ") + + + .. note:: Remember to save both the public and serect keys in a file to use in future. + + +.. class:: Johnny(filepath) + + It creates an object of type `Johnny`, you can provide path to the either public key, or the private key based on the operation + you want to do. + + .. method:: encrypt_bytes(data: bytes, armor=False) + + This method encrypts the given bytes and returns the encrypted bytes. If you pass `armor=True` to the method, then the + returned value will be ascii armored bytes. + + :: + + >>> j = jce.Johnny("tests/files/public.asc") + >>> enc = j.encrypt_bytes(b"mysecret", armor=True) + + + .. method:: encrypt_file(inputfile: bytes, output: bytes, armor=False) + + This method encrypts the given inputfile and writes the raw encrypted bytes to the output path. If you pass `armor=True` to the method, then the + output file will be written as ascii armored. + + :: + + >>> j = jce.Johnny("tests/files/public.asc") + >>> enc = j.encrypt_file(b"blueleaks.tar.gz", b"notblueleaks.tar.gz.pgp", armor=True) + + + .. method:: decrypt_bytes(data: bytes, password: str) + + Decrypts the given bytes based on the secret key and given password. If you try to decrypt while just using the public key, + then it will raise `AttributeError`. + + :: + + >>> jp = jce.Johnny("tests/files/secret.asc") + >>> result = jp.decrypt_bytes(enc, "redhat") + + + .. method:: decrypt_file(inputfile: bytes, output: bytes, password: str) + + Decrypts the inputfile path (in bytes) and wrties the decrypted data to the `output` file. Both the filepaths to be given as bytes. + + :: + + >>> jp = jce.Johnny("tests/files/secret.asc") + >>> result = jp.decrypt_file(b"notblueleaks.tar.gz.pgp", "blueleaks.tar.gz", "redhat") + + + .. method:: sign_bytes_detached(data: bytes, pasword: str) + + Signs the given bytes and returns the detached ascii armored signature as bytes. + + :: + + >>> j = jce.Johnny("tests/files/secret.asc") + >>> signature = j.sign_bytes_detached(b"mysecret", "redhat") + + .. note:: Remember to save the signature somewhere on disk. + + .. method:: verify_bytes(data: bytes, signature: bytes) + + Verifies if the signature is correct for the given data (as bytes). Returns `True` or `False`. + + :: + + >>> j = jce.Johnny("tests/files/secret.asc") + >>> j.verify_bytes(encrypted_bytes, signature) diff --git a/docs/build.rst b/docs/build.rst new file mode 100644 index 0000000..12bf16c --- /dev/null +++ b/docs/build.rst @@ -0,0 +1,31 @@ +Building Johnny Can Encrypt +============================ + +Building this module requires Rust's nightly toolchain. You can install it following +the instructions from `rustup.rs `_. + +You will need `libnettle` and `libnettle-dev` & `clang` (on Debian/Ubuntu) and `nettle` & `nettle-dev` & `clang` packages in Fedora. + +Then you can follow the steps below to build a wheel. + +:: + + python3 -m venv .venv + source .venv/bin/activate + python3 -m pip install requirements-dev.txt + maturin build + +Only to build and test locally, you should execute + +:: + + maturin develop + +## How to run the tests? + +After you did the `maturin develop` as mentioned above, execute the following command. + +:: + + python3 -m pytest -vvv + diff --git a/docs/index.rst b/docs/index.rst index 541efab..1e13dd0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,10 +6,16 @@ Welcome to Johnny Can Encrypt's documentation! ============================================== +This is a Python module providing encryption and decryption operations based on OpenPGP. It +uses `sequoia-pgp `_ project for the actual operations. This module +is written. + .. toctree:: :maxdepth: 2 :caption: Contents: + build + api Indices and tables diff --git a/src/lib.rs b/src/lib.rs index 6967729..9f0dc0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -253,7 +253,12 @@ impl Johnny { Ok(Johnny { filepath, cert }) } - pub fn encrypt_bytes(&self, data: Vec) -> PyResult { + pub fn encrypt_bytes( + &self, + py: Python, + data: Vec, + armor: Option, + ) -> PyResult { let mode = KeyFlags::default().set_storage_encryption(true); let p = &P::new(); let recipients = self @@ -263,11 +268,15 @@ impl Johnny { .alive() .revoked(false) .key_flags(&mode); + // TODO: Find better way to do this in rust let mut result = Vec::new(); - let mut sink = armor::Writer::new(&mut result, armor::Kind::Message)?; + let mut result2 = Vec::new(); + let mut sink = armor::Writer::new(&mut result2, armor::Kind::Message)?; // Stream an OpenPGP message. - let message = Message::new(&mut sink); - + let message = match armor { + Some(true) => Message::new(&mut sink), + _ => Message::new(&mut result), + }; // We want to encrypt a literal data packet. let encryptor = Encryptor::for_recipients(message, recipients) .build() @@ -285,9 +294,18 @@ impl Johnny { // writer stack. literal_writer.finalize().unwrap(); - // Finalize the armor writer. - sink.finalize().expect("Failed to write data"); - Ok(str::from_utf8(&result).unwrap().to_string()) + match armor { + Some(true) => { + // Finalize the armor writer. + sink.finalize().expect("Failed to write data"); + let res = PyBytes::new(py, &result2); + return Ok(res.into()); + } + _ => { + let res = PyBytes::new(py, &result); + return Ok(res.into()); + } + } } pub fn decrypt_bytes(&self, py: Python, data: Vec, password: String) -> PyResult { @@ -303,7 +321,7 @@ impl Johnny { let res = PyBytes::new(py, &result); Ok(res.into()) } - pub fn encrypt_file(&self, filepath: Vec, output: Vec) -> PyResult { + pub fn encrypt_file(&self, filepath: Vec, output: Vec, armor: Option) -> PyResult { let mode = KeyFlags::default().set_storage_encryption(true); let p = &P::new(); let recipients = self @@ -315,28 +333,55 @@ impl Johnny { .key_flags(&mode); let mut input = File::open(str::from_utf8(&filepath[..]).unwrap()).unwrap(); let mut outfile = File::create(str::from_utf8(&output[..]).unwrap()).unwrap(); - // Stream an OpenPGP message. - let message = Message::new(&mut outfile); - - // We want to encrypt a literal data packet. - let encryptor = Encryptor::for_recipients(message, recipients) - .build() - .expect("Failed to create encryptor"); - - let mut literal_writer = LiteralWriter::new(encryptor) - .build() - .expect("Failed to create literal writer"); - - // Copy stdin to our writer stack to encrypt the data. - io::copy(&mut input, &mut literal_writer).expect("Failed to encrypt"); - //literal_writer.write_all(&data).unwrap(); - - // Finally, finalize the OpenPGP message by tearing down the - // writer stack. - literal_writer.finalize().unwrap(); + // TODO: Find better ways to write this code + match armor { + // For armored output file. + Some(true) => { + let mut sink = armor::Writer::new(&mut outfile, armor::Kind::Message).unwrap(); + // Stream an OpenPGP message. + let message = Message::new(&mut sink); + + // We want to encrypt a literal data packet. + let encryptor = Encryptor::for_recipients(message, recipients) + .build() + .expect("Failed to create encryptor"); + + let mut literal_writer = LiteralWriter::new(encryptor) + .build() + .expect("Failed to create literal writer"); + + // Copy stdin to our writer stack to encrypt the data. + io::copy(&mut input, &mut literal_writer).expect("Failed to encrypt"); + //literal_writer.write_all(&data).unwrap(); + + // Finally, finalize the OpenPGP message by tearing down the + // writer stack. + literal_writer.finalize().unwrap(); + + // Finalize the armor writer. + sink.finalize().expect("Failed to write data");} + _ => { + let message = Message::new(&mut outfile); + + // We want to encrypt a literal data packet. + let encryptor = Encryptor::for_recipients(message, recipients) + .build() + .expect("Failed to create encryptor"); + + let mut literal_writer = LiteralWriter::new(encryptor) + .build() + .expect("Failed to create literal writer"); + + // Copy stdin to our writer stack to encrypt the data. + io::copy(&mut input, &mut literal_writer).expect("Failed to encrypt"); + //literal_writer.write_all(&data).unwrap(); + + // Finally, finalize the OpenPGP message by tearing down the + // writer stack. + literal_writer.finalize().unwrap(); + } + } - // Finalize the armor writer. - //sink.finalize().expect("Failed to write data"); Ok(true) } diff --git a/tests/test_encrypt_decrypt.py b/tests/test_encrypt_decrypt.py index fd2ba39..ab48fdb 100644 --- a/tests/test_encrypt_decrypt.py +++ b/tests/test_encrypt_decrypt.py @@ -3,34 +3,71 @@ DATA= "Kushal loves 🦀" +def clean_outputfiles(output, decrypted_output): + # Remove any existing test files + if os.path.exists(output): + os.remove(output) + if os.path.exists(decrypted_output): + os.remove(decrypted_output) + +def verify_files(inputfile, decrypted_output): + # read both the files + with open(inputfile) as f: + original_text = f.read() + + with open(decrypted_output) as f: + decrypted_text = f.read() + assert original_text == decrypted_text + + def test_encrypt_decrypt_bytes(): + "Tests raw bytes as output" j = jce.Johnny("tests/files/public.asc") enc = j.encrypt_bytes(DATA.encode("utf-8")) jp = jce.Johnny("tests/files/secret.asc") - result = jp.decrypt_bytes(enc.encode("utf-8"), "redhat") + result = jp.decrypt_bytes(enc, "redhat") assert DATA == result.decode("utf-8") +def test_encrypt_decrypt_bytes_armored(): + "Tests ascii-armored output" + j = jce.Johnny("tests/files/public.asc") + enc = j.encrypt_bytes(DATA.encode("utf-8"), armor=True) + assert enc.startswith(b"-----BEGIN PGP MESSAGE-----") + jp = jce.Johnny("tests/files/secret.asc") + result = jp.decrypt_bytes(enc, "redhat") + assert DATA == result.decode("utf-8") + + + def test_encrypt_decrypt_files(): + "Tests encrypt/decrypt file in binary format" inputfile = "tests/files/text.txt" output = "/tmp/text-encrypted.pgp" decrypted_output = "/tmp/text.txt" - # Remove any existing test files - if os.path.exists(output): - os.remove(output) - if os.path.exists(decrypted_output): - os.remove(decrypted_output) + clean_outputfiles(output, decrypted_output) # Now encrypt and then decrypt j = jce.Johnny("tests/files/public.asc") assert j.encrypt_file(inputfile.encode("utf-8"), output.encode("utf-8")) jp = jce.Johnny("tests/files/secret.asc") - result = jp.decrypt_file(output.encode("utf-8"), decrypted_output.encode("utf-8"), "redhat") - # read both the files - with open(inputfile) as f: - original_text = f.read() + assert jp.decrypt_file(output.encode("utf-8"), decrypted_output.encode("utf-8"), "redhat") - with open(decrypted_output) as f: - decrypted_text = f.read() + verify_files(inputfile, decrypted_output) - assert original_text == decrypted_text +def test_encrypt_decrypt_files_armored(): + inputfile = "tests/files/text.txt" + output = "/tmp/text-encrypted.asc" + decrypted_output = "/tmp/text.txt" + clean_outputfiles(output, decrypted_output) + + # Now encrypt and then decrypt + j = jce.Johnny("tests/files/public.asc") + assert j.encrypt_file(inputfile.encode("utf-8"), output.encode("utf-8"), armor=True) + jp = jce.Johnny("tests/files/secret.asc") + assert jp.decrypt_file(output.encode("utf-8"), decrypted_output.encode("utf-8"), "redhat") + + with open(output) as f: + line = f.readline().strip("\n") + assert line == "-----BEGIN PGP MESSAGE-----" + verify_files(inputfile, decrypted_output)