Add GPG signing and sig verifying

embed
Manos Pitsidianakis 2019-09-28 10:46:49 +03:00
parent 963fdd1575
commit e35a93336a
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
11 changed files with 391 additions and 86 deletions

2
meli.1
View File

@ -180,6 +180,8 @@ in composer, add
as an attachment
.It Ic remove-attachment Ar INDEX
remove attachment with given index
.It Ic toggle sign
toggle between signing and not signing this message. If the gpg invocation fails then the mail won't be sent.
.El
.Pp
generic commands:

View File

@ -74,6 +74,27 @@ impl<'a> From<&'a [u8]> for Charset {
}
}
impl Display for Charset {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
match self {
Charset::Ascii => write!(f, "us-ascii"),
Charset::UTF8 => write!(f, "utf-8"),
Charset::UTF16 => write!(f, "utf-16"),
Charset::ISO8859_1 => write!(f, "iso-8859-1"),
Charset::ISO8859_2 => write!(f, "iso-8859-2"),
Charset::ISO8859_7 => write!(f, "iso-8859-7"),
Charset::ISO8859_15 => write!(f, "iso-8859-15"),
Charset::Windows1251 => write!(f, "windows-1251"),
Charset::Windows1252 => write!(f, "windows-1252"),
Charset::Windows1253 => write!(f, "windows-1253"),
Charset::GBK => write!(f, "GBK"),
Charset::GB2312 => write!(f, "gb2312"),
Charset::BIG5 => write!(f, "BIG5"),
Charset::ISO2022JP => write!(f, "ISO-2022-JP"),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum MultipartType {
Mixed,
@ -264,7 +285,7 @@ pub enum ContentTransferEncoding {
impl Default for ContentTransferEncoding {
fn default() -> Self {
ContentTransferEncoding::_7Bit
ContentTransferEncoding::_8Bit
}
}

View File

@ -273,6 +273,23 @@ impl From<Attachment> for AttachmentBuilder {
}
}
impl From<AttachmentBuilder> for Attachment {
fn from(val: AttachmentBuilder) -> Self {
let AttachmentBuilder {
content_type,
content_transfer_encoding,
raw,
body,
} = val;
Attachment {
content_type,
content_transfer_encoding,
raw,
body,
}
}
}
/// Immutable attachment type.
#[derive(Clone, Serialize, Deserialize, PartialEq)]
pub struct Attachment {
@ -546,6 +563,73 @@ impl Attachment {
_ => false,
}
}
pub fn into_raw(&self) -> String {
let mut ret = String::with_capacity(2 * self.raw.len());
fn into_raw_helper(a: &Attachment, ret: &mut String) {
ret.extend(
format!(
"Content-Transfer-Encoding: {}\n",
a.content_transfer_encoding
)
.chars(),
);
match &a.content_type {
ContentType::Text { kind: _, charset } => {
ret.extend(
format!("Content-Type: {}; charset={}\n\n", a.content_type, charset)
.chars(),
);
ret.extend(String::from_utf8_lossy(a.body()).chars());
}
ContentType::Multipart {
boundary,
kind,
parts,
} => {
let boundary = String::from_utf8_lossy(boundary);
ret.extend(format!("Content-Type: {}; boundary={}", kind, boundary).chars());
if *kind == MultipartType::Signed {
ret.extend(
"; micalg=pgp-sha512; protocol=\"application/pgp-signature\"".chars(),
);
}
ret.push('\n');
let boundary_start = format!("\n--{}\n", boundary);
for p in parts {
ret.extend(boundary_start.chars());
into_raw_helper(p, ret);
}
ret.extend(format!("--{}--\n\n", boundary).chars());
}
ContentType::MessageRfc822 => {
ret.extend(format!("Content-Type: {}\n\n", a.content_type).chars());
ret.extend(String::from_utf8_lossy(a.body()).chars());
}
ContentType::PGPSignature => {
ret.extend(format!("Content-Type: {}\n\n", a.content_type).chars());
ret.extend(String::from_utf8_lossy(a.body()).chars());
}
ContentType::OctetStream { ref name } => {
if let Some(name) = name {
ret.extend(
format!("Content-Type: {}; name={}\n\n", a.content_type, name).chars(),
);
} else {
ret.extend(format!("Content-Type: {}\n\n", a.content_type).chars());
}
ret.push_str(&BASE64_MIME.encode(a.body()).trim());
}
_ => {
ret.extend(format!("Content-Type: {}\n\n", a.content_type).chars());
ret.extend(String::from_utf8_lossy(a.body()).chars());
}
}
}
into_raw_helper(self, &mut ret);
ret
}
}
pub fn interpret_format_flowed(_t: &str) -> String {

View File

@ -18,11 +18,11 @@ use fnv::FnvHashMap;
#[derive(Debug, PartialEq, Clone)]
pub struct Draft {
headers: FnvHashMap<String, String>,
header_order: Vec<String>,
body: String,
pub headers: FnvHashMap<String, String>,
pub header_order: Vec<String>,
pub body: String,
attachments: Vec<AttachmentBuilder>,
pub attachments: Vec<AttachmentBuilder>,
}
impl Default for Draft {
@ -259,7 +259,19 @@ impl Draft {
}
ret.push_str("MIME-Version: 1.0\n");
if !self.attachments.is_empty() {
if self.attachments.is_empty() {
let content_type: ContentType = Default::default();
let content_transfer_encoding: ContentTransferEncoding = ContentTransferEncoding::_8Bit;
ret.extend(format!("Content-Type: {}; charset=\"utf-8\"\n", content_type).chars());
ret.extend(
format!("Content-Transfer-Encoding: {}\n", content_transfer_encoding).chars(),
);
ret.push('\n');
ret.push_str(&self.body);
} else if self.attachments.len() == 1 && self.body.is_empty() {
let attachment: Attachment = self.attachments.remove(0).into();
ret.extend(attachment.into_raw().chars());
} else {
let mut parts = Vec::with_capacity(self.attachments.len() + 1);
let attachments = std::mem::replace(&mut self.attachments, Vec::new());
let mut body_attachment = AttachmentBuilder::default();
@ -267,24 +279,6 @@ impl Draft {
parts.push(body_attachment);
parts.extend(attachments.into_iter());
build_multipart(&mut ret, MultipartType::Mixed, parts);
} else {
if self.body.is_ascii() {
ret.push('\n');
ret.push_str(&self.body);
} else {
let content_type: ContentType = Default::default();
let content_transfer_encoding: ContentTransferEncoding =
ContentTransferEncoding::Base64;
ret.extend(format!("Content-Type: {}; charset=\"utf-8\"\n", content_type).chars());
ret.extend(
format!("Content-Transfer-Encoding: {}\n", content_transfer_encoding).chars(),
);
ret.push('\n');
ret.push_str(&BASE64_MIME.encode(&self.body.as_bytes()).trim());
ret.push('\n');
}
}
Ok(ret)

View File

@ -33,6 +33,8 @@ pub use crate::view::*;
mod compose;
pub use self::compose::*;
pub mod pgp;
mod accounts;
pub use self::accounts::*;

View File

@ -44,6 +44,7 @@ pub struct Composer {
form: FormWidget,
mode: ViewMode,
sign_mail: ToggleFlag,
dirty: bool,
initialized: bool,
id: ComponentId,
@ -62,6 +63,7 @@ impl Default for Composer {
form: FormWidget::default(),
mode: ViewMode::Edit,
sign_mail: ToggleFlag::Unset,
dirty: true,
initialized: false,
id: ComponentId::new_v4(),
@ -217,11 +219,20 @@ impl Composer {
}
}
fn draw_attachments(&self, grid: &mut CellBuffer, area: Area, _context: &mut Context) {
fn draw_attachments(&self, grid: &mut CellBuffer, area: Area, context: &Context) {
let attachments_no = self.draft.attachments().len();
if attachments_no == 0 {
if self.sign_mail.is_true() {
write_string_to_grid(
"no attachments",
&format!(
"☑ sign with {}",
context
.settings
.pgp
.key
.as_ref()
.map(String::as_str)
.unwrap_or("default key")
),
grid,
Color::Default,
Color::Default,
@ -231,7 +242,7 @@ impl Composer {
);
} else {
write_string_to_grid(
&format!("{} attachments ", attachments_no),
"☐ don't sign",
grid,
Color::Default,
Color::Default,
@ -239,6 +250,27 @@ impl Composer {
(pos_inc(upper_left!(area), (0, 1)), bottom_right!(area)),
false,
);
}
if attachments_no == 0 {
write_string_to_grid(
"no attachments",
grid,
Color::Default,
Color::Default,
Attr::Default,
(pos_inc(upper_left!(area), (0, 2)), bottom_right!(area)),
false,
);
} else {
write_string_to_grid(
&format!("{} attachments ", attachments_no),
grid,
Color::Default,
Color::Default,
Attr::Default,
(pos_inc(upper_left!(area), (0, 2)), bottom_right!(area)),
false,
);
for (i, a) in self.draft.attachments().iter().enumerate() {
if let Some(name) = a.content_type().name() {
write_string_to_grid(
@ -253,7 +285,7 @@ impl Composer {
Color::Default,
Color::Default,
Attr::Default,
(pos_inc(upper_left!(area), (0, 2 + i)), bottom_right!(area)),
(pos_inc(upper_left!(area), (0, 3 + i)), bottom_right!(area)),
false,
);
} else {
@ -263,7 +295,7 @@ impl Composer {
Color::Default,
Color::Default,
Attr::Default,
(pos_inc(upper_left!(area), (0, 2 + i)), bottom_right!(area)),
(pos_inc(upper_left!(area), (0, 3 + i)), bottom_right!(area)),
false,
);
}
@ -291,6 +323,9 @@ impl Component for Composer {
};
if !self.initialized {
if self.sign_mail.is_unset() {
self.sign_mail = ToggleFlag::InternalVal(context.settings.pgp.auto_sign);
}
if !self.draft.headers().contains_key("From") || self.draft.headers()["From"].is_empty()
{
self.draft.headers_mut().insert(
@ -632,7 +667,12 @@ impl Component for Composer {
}
UIEvent::Input(Key::Char('s')) => {
self.update_draft();
if send_draft(context, self.account_cursor, self.draft.clone()) {
if send_draft(
self.sign_mail,
context,
self.account_cursor,
self.draft.clone(),
) {
context
.replies
.push_back(UIEvent::Action(Tab(Kill(self.id))));
@ -743,6 +783,12 @@ impl Component for Composer {
self.dirty = true;
return true;
}
Action::Compose(ComposeAction::ToggleSign) => {
let is_true = self.sign_mail.is_true();
self.sign_mail = ToggleFlag::from(!is_true);
self.dirty = true;
return true;
}
_ => {}
}
}
@ -815,7 +861,12 @@ impl Component for Composer {
}
}
pub fn send_draft(context: &mut Context, account_cursor: usize, draft: Draft) -> bool {
pub fn send_draft(
sign_mail: ToggleFlag,
context: &mut Context,
account_cursor: usize,
mut draft: Draft,
) -> bool {
use std::io::Write;
use std::process::{Command, Stdio};
let mut failure = true;
@ -830,6 +881,55 @@ pub fn send_draft(context: &mut Context, account_cursor: usize, draft: Draft) ->
.expect("Failed to start mailer command");
{
let stdin = msmtp.stdin.as_mut().expect("failed to open stdin");
if sign_mail.is_true() {
let mut body: AttachmentBuilder = Attachment::new(
Default::default(),
Default::default(),
std::mem::replace(&mut draft.body, String::new()).into_bytes(),
)
.into();
if !draft.attachments.is_empty() {
let mut parts = std::mem::replace(&mut draft.attachments, Vec::new());
parts.insert(0, body);
let boundary = ContentType::make_boundary(&parts);
body = Attachment::new(
ContentType::Multipart {
boundary: boundary.into_bytes(),
kind: MultipartType::Mixed,
parts: parts.into_iter().map(|a| a.into()).collect::<Vec<_>>(),
},
Default::default(),
Vec::new(),
)
.into();
}
let output = crate::components::mail::pgp::sign(
body.into(),
context.settings.pgp.gpg_binary.as_ref().map(String::as_str),
context.settings.pgp.key.as_ref().map(String::as_str),
);
if let Err(e) = &output {
debug!("{:?} could not sign draft msg", e);
log(
format!(
"Could not sign draft in account `{}`: {}.",
context.accounts[account_cursor].name(),
e.to_string()
),
ERROR,
);
context.replies.push_back(UIEvent::Notification(
Some(format!(
"Could not sign draft in account `{}`.",
context.accounts[account_cursor].name()
)),
e.to_string(),
Some(NotificationType::ERROR),
));
return false;
}
draft.attachments.push(output.unwrap());
}
let draft = draft.finalise().unwrap();
stdin
.write_all(draft.as_bytes())

View File

@ -0,0 +1,130 @@
/*
* meli - ui crate.
*
* Copyright 2019 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use super::*;
use std::io::Write;
use std::process::{Command, Stdio};
pub fn verify_signature(a: &Attachment, context: &mut Context) -> Vec<u8> {
match melib::signatures::verify_signature(a) {
Ok((bytes, sig)) => {
let bytes_file = create_temp_file(&bytes, None, None, true);
let signature_file = create_temp_file(sig, None, None, true);
if let Ok(gpg) = Command::new(
context
.settings
.pgp
.gpg_binary
.as_ref()
.map(String::as_str)
.unwrap_or("gpg2"),
)
.args(&[
"--output",
"-",
"--verify",
signature_file.path.to_str().unwrap(),
bytes_file.path.to_str().unwrap(),
])
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
{
return gpg.wait_with_output().unwrap().stderr;
} else {
context.replies.push_back(UIEvent::Notification(
Some(format!(
"Failed to launch {} to verify PGP signature",
context
.settings
.pgp
.gpg_binary
.as_ref()
.map(String::as_str)
.unwrap_or("gpg2"),
)),
"see meli.conf(5) for configuration setting pgp.gpg_binary".to_string(),
Some(NotificationType::ERROR),
));
}
}
Err(e) => {
context.replies.push_back(UIEvent::Notification(
Some(e.to_string()),
String::new(),
Some(NotificationType::ERROR),
));
}
}
Vec::new()
}
/// Returns multipart/signed
pub fn sign(
a: AttachmentBuilder,
gpg_binary: Option<&str>,
pgp_key: Option<&str>,
) -> Result<AttachmentBuilder> {
let mut command = Command::new(gpg_binary.unwrap_or("gpg2"));
command.args(&[
"--digest-algo",
"sha512",
"--output",
"-",
"--detach-sig",
"--armor",
]);
if let Some(key) = pgp_key {
command.args(&["--local-user", key]);
}
let a: Attachment = a.into();
let mut gpg = command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()?;
let sig_attachment = {
gpg.stdin
.as_mut()
.unwrap()
.write_all(&melib::signatures::convert_attachment_to_rfc_spec(
a.into_raw().as_bytes(),
))
.unwrap();
let gpg = gpg.wait_with_output().unwrap();
Attachment::new(ContentType::PGPSignature, Default::default(), gpg.stdout)
};
let a: AttachmentBuilder = a.into();
let parts = vec![a, sig_attachment.into()];
let boundary = ContentType::make_boundary(&parts);
Ok(Attachment::new(
ContentType::Multipart {
boundary: boundary.into_bytes(),
kind: MultipartType::Signed,
parts: parts.into_iter().map(|a| a.into()).collect::<Vec<_>>(),
},
Default::default(),
Vec::new(),
)
.into())
}

View File

@ -196,58 +196,7 @@ impl MailView {
} else if a.is_signed() {
v.clear();
if context.settings.pgp.auto_verify_signatures {
match melib::signatures::verify_signature(a) {
Ok((bytes, sig)) => {
let bytes_file = create_temp_file(&bytes, None, None, true);
let signature_file = create_temp_file(sig, None, None, true);
if let Ok(gpg) = Command::new(
context
.settings
.pgp
.gpg_binary
.as_ref()
.map(String::as_str)
.unwrap_or("gpg2"),
)
.args(&[
"--output",
"-",
"--verify",
signature_file.path.to_str().unwrap(),
bytes_file.path.to_str().unwrap(),
])
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
{
v.extend(gpg.wait_with_output().unwrap().stderr);
} else {
context.replies.push_back(UIEvent::Notification(
Some(format!(
"Failed to launch {} to verify PGP signature",
context
.settings
.pgp
.gpg_binary
.as_ref()
.map(String::as_str)
.unwrap_or("gpg2"),
)),
"see meli.conf(5) for configuration setting pgp.gpg_binary"
.to_string(),
Some(NotificationType::ERROR),
));
return;
}
}
Err(e) => {
context.replies.push_back(UIEvent::Notification(
Some(e.to_string()),
String::new(),
Some(NotificationType::ERROR),
));
}
}
v.extend(crate::mail::pgp::verify_signature(a, context).into_iter());
}
}
})),
@ -1041,6 +990,7 @@ impl Component for MailView {
),
);
if super::compose::send_draft(
ToggleFlag::False,
/* FIXME: refactor to avoid unsafe.
*
* actions contains byte slices from the envelope's

View File

@ -58,7 +58,7 @@ macro_rules! split_command {
}};
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Copy, Debug, Clone, PartialEq)]
pub enum ToggleFlag {
Unset,
InternalVal(bool),
@ -66,6 +66,16 @@ pub enum ToggleFlag {
True,
}
impl From<bool> for ToggleFlag {
fn from(val: bool) -> Self {
if val {
ToggleFlag::True
} else {
ToggleFlag::False
}
}
}
impl Default for ToggleFlag {
fn default() -> Self {
ToggleFlag::Unset

View File

@ -214,6 +214,17 @@ define_commands!([
);
)
},
{ tags: ["toggle sign "],
desc: "switch between sign/unsign for this draft",
parser:(
named!( toggle_sign<Action>,
do_parse!(
ws!(tag!("toggle sign"))
>> (Compose(ToggleSign))
)
);
)
},
{ tags: ["create-folder "],
desc: "create-folder ACCOUNT FOLDER_PATH",
parser:(
@ -350,7 +361,7 @@ named!(
named!(
compose_action<Action>,
alt_complete!(add_attachment | remove_attachment)
alt_complete!(add_attachment | remove_attachment | toggle_sign)
);
named!(pub parse_command<Action>,

View File

@ -71,6 +71,7 @@ pub enum PagerAction {
pub enum ComposeAction {
AddAttachment(String),
RemoveAttachment(usize),
ToggleSign,
}
#[derive(Debug)]