Skip to content

Commit 5233b87

Browse files
authored
Merge pull request #53 from dnet/main
Added options for armor-less output and file-based I/O
2 parents 37cb423 + 8679e8c commit 5233b87

File tree

5 files changed

+288
-12
lines changed

5 files changed

+288
-12
lines changed

README.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,41 @@ print(f"Clear signed: {clear}")
8888
assert "PGP SIGNED MESSAGE" in str(clear)
8989
```
9090

91+
### sign_file
92+
93+
Signs data from a file and writes the signed output to another file:
94+
95+
```python
96+
from pysequoia import sign_file, SignatureMode
97+
import tempfile, os
98+
99+
s = Cert.from_file("signing-key.asc")
100+
101+
# create a file with data to sign
102+
with tempfile.NamedTemporaryFile(delete=False, suffix=".txt") as inp:
103+
inp.write("data to be signed".encode("utf8"))
104+
input_path = inp.name
105+
106+
with tempfile.NamedTemporaryFile(delete=False, suffix=".pgp") as out:
107+
output_path = out.name
108+
109+
sign_file(s.secrets.signer(), input_path, output_path)
110+
signed = open(output_path, "rb").read()
111+
assert b"PGP MESSAGE" in signed
112+
113+
# detached signature to file
114+
with tempfile.NamedTemporaryFile(delete=False, suffix=".sig") as out:
115+
detached_path = out.name
116+
117+
sign_file(s.secrets.signer(), input_path, detached_path, mode=SignatureMode.DETACHED)
118+
detached = open(detached_path, "rb").read()
119+
assert b"PGP SIGNATURE" in detached
120+
121+
os.unlink(input_path)
122+
os.unlink(output_path)
123+
os.unlink(detached_path)
124+
```
125+
91126
### verify
92127

93128
Verifies signed data and returns verified data:
@@ -167,6 +202,33 @@ print(f"Encrypted data: {encrypted}")
167202

168203
The `signer` argument is optional and when omitted the function will return an unsigned (but encrypted) message.
169204

205+
### encrypt_file
206+
207+
Encrypts data from a file and writes the encrypted output to another file:
208+
209+
```python
210+
from pysequoia import encrypt_file
211+
import tempfile, os
212+
213+
s = Cert.from_file("passwd.pgp")
214+
r = Cert.from_bytes(open("wiktor.asc", "rb").read())
215+
216+
# create a file with content to encrypt
217+
with tempfile.NamedTemporaryFile(delete=False, suffix=".txt") as inp:
218+
inp.write("content to encrypt".encode("utf8"))
219+
input_path = inp.name
220+
221+
with tempfile.NamedTemporaryFile(delete=False, suffix=".pgp") as out:
222+
output_path = out.name
223+
224+
encrypt_file(signer = s.secrets.signer("hunter22"), recipients = [r], input = input_path, output = output_path)
225+
encrypted = open(output_path, "rb").read()
226+
assert b"PGP MESSAGE" in encrypted
227+
228+
os.unlink(input_path)
229+
os.unlink(output_path)
230+
```
231+
170232
### decrypt
171233

172234
Decrypts plain data:
@@ -216,6 +278,81 @@ assert decrypted.valid_sigs[0].signing_key == sender.fingerprint
216278

217279
Here, the same remarks as to [`verify`](#verify) also apply.
218280

281+
### decrypt_file
282+
283+
Decrypts data from a file and writes the decrypted output to another file:
284+
285+
```python
286+
from pysequoia import decrypt_file
287+
import tempfile, os
288+
289+
sender = Cert.from_file("no-passwd.pgp")
290+
receiver = Cert.from_file("passwd.pgp")
291+
292+
content = "Red Green Blue"
293+
294+
encrypted = encrypt(recipients = [receiver], bytes = content.encode("utf8"))
295+
296+
# write encrypted data to a file
297+
with tempfile.NamedTemporaryFile(delete=False, suffix=".pgp") as inp:
298+
inp.write(encrypted)
299+
input_path = inp.name
300+
301+
with tempfile.NamedTemporaryFile(delete=False, suffix=".txt") as out:
302+
output_path = out.name
303+
304+
decrypted = decrypt_file(decryptor = receiver.secrets.decryptor("hunter22"), input = input_path, output = output_path)
305+
306+
# content is written to the output file, not returned in memory
307+
assert decrypted.bytes is None
308+
309+
# read decrypted content from the output file
310+
assert open(output_path, "rb").read().decode("utf8") == content
311+
312+
# this message did not contain any valid signatures
313+
assert len(decrypted.valid_sigs) == 0
314+
315+
os.unlink(input_path)
316+
os.unlink(output_path)
317+
```
318+
319+
Decrypt file can also verify signatures while decrypting:
320+
321+
```python
322+
from pysequoia import decrypt_file
323+
import tempfile, os
324+
325+
sender = Cert.from_file("no-passwd.pgp")
326+
receiver = Cert.from_file("passwd.pgp")
327+
328+
content = "Red Green Blue"
329+
330+
encrypted = encrypt(signer = sender.secrets.signer(), recipients = [receiver], bytes = content.encode("utf8"))
331+
332+
# write encrypted data to a file
333+
with tempfile.NamedTemporaryFile(delete=False, suffix=".pgp") as inp:
334+
inp.write(encrypted)
335+
input_path = inp.name
336+
337+
with tempfile.NamedTemporaryFile(delete=False, suffix=".txt") as out:
338+
output_path = out.name
339+
340+
def get_certs(key_ids):
341+
print(f"For verification after decryption, we need these keys: {key_ids}")
342+
return [sender]
343+
344+
decrypted = decrypt_file(decryptor = receiver.secrets.decryptor("hunter22"), input = input_path, output = output_path, store = get_certs)
345+
346+
assert open(output_path, "rb").read().decode("utf8") == content
347+
348+
# let's check the valid signature's certificate and signing subkey fingerprints
349+
assert decrypted.valid_sigs[0].certificate == sender.fingerprint
350+
assert decrypted.valid_sigs[0].signing_key == sender.fingerprint
351+
352+
os.unlink(input_path)
353+
os.unlink(output_path)
354+
```
355+
219356
## Certificates
220357

221358
The `Cert` class represents one OpenPGP certificate (commonly called a

src/decrypt.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
use std::path::PathBuf;
12
use std::sync::{Arc, Mutex};
23

4+
use anyhow::Context;
35
use pyo3::prelude::*;
46
use sequoia_openpgp::crypto::{Decryptor, SessionKey};
57
use sequoia_openpgp::packet::{PKESK, SKESK};
@@ -63,6 +65,32 @@ pub fn decrypt(
6365
})
6466
}
6567

68+
#[pyfunction]
69+
#[pyo3(signature = (decryptor, input, output, store=None))]
70+
pub fn decrypt_file(
71+
mut decryptor: PyDecryptor,
72+
input: PathBuf,
73+
output: PathBuf,
74+
store: Option<Py<PyAny>>,
75+
) -> PyResult<Decrypted> {
76+
if let Some(store) = store {
77+
decryptor.set_verifier(PyVerifier::from_callback(store));
78+
}
79+
let policy = &P::new();
80+
81+
let mut decryptor = DecryptorBuilder::from_file(&input)
82+
.context("Failed to open input file")?
83+
.with_policy(policy, None, decryptor)?;
84+
85+
let mut sink = std::fs::File::create(&output).context("Failed to create output file")?;
86+
std::io::copy(&mut decryptor, &mut sink)?;
87+
let decryptor = decryptor.into_helper();
88+
Ok(Decrypted {
89+
content: None,
90+
valid_sigs: decryptor.valid_sigs(),
91+
})
92+
}
93+
6694
impl VerificationHelper for PyDecryptor {
6795
fn get_certs(&mut self, ids: &[KeyHandle]) -> sequoia_openpgp::Result<Vec<cert::Cert>> {
6896
if let Some(verifier) = &mut self.verifier {

src/encrypt.rs

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::borrow::Cow;
22
use std::io::Write;
3+
use std::path::PathBuf;
34

45
use anyhow::Context;
56
use pyo3::prelude::*;
@@ -13,13 +14,15 @@ use sequoia_openpgp::types::KeyFlags;
1314
use crate::cert::Cert;
1415
use crate::signer::PySigner;
1516

16-
#[pyfunction]
17-
#[pyo3(signature = (recipients, bytes, signer=None))]
18-
pub fn encrypt(
19-
recipients: Vec<PyRef<Cert>>,
20-
bytes: &[u8],
21-
signer: Option<PySigner>,
22-
) -> PyResult<Cow<'static, [u8]>> {
17+
type RecipientKey = (
18+
Option<sequoia_openpgp::types::Features>,
19+
sequoia_openpgp::packet::Key<
20+
sequoia_openpgp::packet::key::PublicParts,
21+
sequoia_openpgp::packet::key::UnspecifiedRole,
22+
>,
23+
);
24+
25+
fn resolve_recipient_keys(recipients: &[PyRef<Cert>]) -> PyResult<Vec<RecipientKey>> {
2326
let mode = KeyFlags::empty()
2427
.set_storage_encryption()
2528
.set_transport_encryption();
@@ -62,12 +65,28 @@ pub fn encrypt(
6265
);
6366
}
6467
}
68+
Ok(recipient_keys)
69+
}
70+
71+
#[pyfunction]
72+
#[pyo3(signature = (recipients, bytes, signer=None, *, armor=true))]
73+
pub fn encrypt(
74+
recipients: Vec<PyRef<Cert>>,
75+
bytes: &[u8],
76+
signer: Option<PySigner>,
77+
armor: bool,
78+
) -> PyResult<Cow<'static, [u8]>> {
79+
let recipient_keys = resolve_recipient_keys(&recipients)?;
6580

6681
let mut sink = vec![];
6782

6883
let message = Message::new(&mut sink);
6984

70-
let message = Armorer::new(message).build()?;
85+
let message = if armor {
86+
Armorer::new(message).build()?
87+
} else {
88+
message
89+
};
7190

7291
let mut message = Encryptor::for_recipients(
7392
message,
@@ -91,3 +110,48 @@ pub fn encrypt(
91110

92111
Ok(sink.into())
93112
}
113+
114+
#[pyfunction]
115+
#[pyo3(signature = (recipients, input, output, signer=None, *, armor=true))]
116+
pub fn encrypt_file(
117+
recipients: Vec<PyRef<Cert>>,
118+
input: PathBuf,
119+
output: PathBuf,
120+
signer: Option<PySigner>,
121+
armor: bool,
122+
) -> PyResult<()> {
123+
let recipient_keys = resolve_recipient_keys(&recipients)?;
124+
125+
let mut sink = std::fs::File::create(&output).context("Failed to create output file")?;
126+
127+
let message = Message::new(&mut sink);
128+
129+
let message = if armor {
130+
Armorer::new(message).build()?
131+
} else {
132+
message
133+
};
134+
135+
let mut message = Encryptor::for_recipients(
136+
message,
137+
recipient_keys
138+
.iter()
139+
.map(|(features, key)| Recipient::new(features.clone(), key.key_handle(), key)),
140+
)
141+
.build()
142+
.context("Failed to create encryptor")?;
143+
144+
if let Some(signer) = signer {
145+
message = Signer::new(message, signer)?.build()?;
146+
}
147+
let mut message = LiteralWriter::new(message)
148+
.build()
149+
.context("Failed to create literal writer")?;
150+
151+
let mut input_file = std::fs::File::open(&input).context("Failed to open input file")?;
152+
std::io::copy(&mut input_file, &mut message)?;
153+
154+
message.finalize()?;
155+
156+
Ok(())
157+
}

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,11 @@ fn pysequoia(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
9898
m.add_class::<notation::Notation>()?;
9999
m.add_class::<sign::SignatureMode>()?;
100100
m.add_function(wrap_pyfunction!(sign::sign, m)?)?;
101+
m.add_function(wrap_pyfunction!(sign::sign_file, m)?)?;
101102
m.add_function(wrap_pyfunction!(encrypt::encrypt, m)?)?;
103+
m.add_function(wrap_pyfunction!(encrypt::encrypt_file, m)?)?;
102104
m.add_function(wrap_pyfunction!(decrypt::decrypt, m)?)?;
105+
m.add_function(wrap_pyfunction!(decrypt::decrypt_file, m)?)?;
103106
m.add_function(wrap_pyfunction!(verify::verify, m)?)?;
104107
Ok(())
105108
}

src/sign.rs

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use std::borrow::Cow;
22
use std::io::Write;
3+
use std::path::PathBuf;
34

5+
use anyhow::Context;
46
use pyo3::prelude::*;
57
use sequoia_openpgp::armor;
68
use sequoia_openpgp::serialize::stream::Armorer;
@@ -20,16 +22,21 @@ pub enum SignatureMode {
2022
}
2123

2224
#[pyfunction]
23-
#[pyo3(signature = (signer, bytes, *, mode=&SignatureMode::Inline))]
24-
pub fn sign(signer: PySigner, bytes: &[u8], mode: &SignatureMode) -> PyResult<Cow<'static, [u8]>> {
25+
#[pyo3(signature = (signer, bytes, *, mode=&SignatureMode::Inline, armor=true))]
26+
pub fn sign(
27+
signer: PySigner,
28+
bytes: &[u8],
29+
mode: &SignatureMode,
30+
armor: bool,
31+
) -> PyResult<Cow<'static, [u8]>> {
2532
use sequoia_openpgp::serialize::stream::Signer;
2633

2734
let mut sink = vec![];
2835
{
2936
let message = Message::new(&mut sink);
30-
let message = if mode == &SignatureMode::Inline {
37+
let message = if mode == &SignatureMode::Inline && armor {
3138
Armorer::new(message).kind(armor::Kind::Message).build()?
32-
} else if mode == &SignatureMode::Detached {
39+
} else if mode == &SignatureMode::Detached && armor {
3340
Armorer::new(message).kind(armor::Kind::Signature).build()?
3441
} else {
3542
message
@@ -48,3 +55,40 @@ pub fn sign(signer: PySigner, bytes: &[u8], mode: &SignatureMode) -> PyResult<Co
4855

4956
Ok(sink.into())
5057
}
58+
59+
#[pyfunction]
60+
#[pyo3(signature = (signer, input, output, *, mode=&SignatureMode::Inline, armor=true))]
61+
pub fn sign_file(
62+
signer: PySigner,
63+
input: PathBuf,
64+
output: PathBuf,
65+
mode: &SignatureMode,
66+
armor: bool,
67+
) -> PyResult<()> {
68+
use sequoia_openpgp::serialize::stream::Signer;
69+
70+
let mut sink = std::fs::File::create(&output).context("Failed to create output file")?;
71+
{
72+
let message = Message::new(&mut sink);
73+
let message = if mode == &SignatureMode::Inline && armor {
74+
Armorer::new(message).kind(armor::Kind::Message).build()?
75+
} else if mode == &SignatureMode::Detached && armor {
76+
Armorer::new(message).kind(armor::Kind::Signature).build()?
77+
} else {
78+
message
79+
};
80+
let message = Signer::new(message, signer)?;
81+
let mut message = if mode == &SignatureMode::Inline {
82+
LiteralWriter::new(message.build()?).build()?
83+
} else if mode == &SignatureMode::Detached {
84+
message.detached().build()?
85+
} else {
86+
message.cleartext().build()?
87+
};
88+
let mut input_file = std::fs::File::open(&input).context("Failed to open input file")?;
89+
std::io::copy(&mut input_file, &mut message)?;
90+
message.finalize()?;
91+
}
92+
93+
Ok(())
94+
}

0 commit comments

Comments
 (0)