| 1 |
=begin |
| 2 |
GPG/MIME Encoder and Decoder. |
| 3 |
|
| 4 |
Classes for the encryption and decryption of email according to RFC3156. |
| 5 |
|
| 6 |
(C) 2009, General Public Licence, Author: Otto Linnemann |
| 7 |
=end |
| 8 |
|
| 9 |
require 'digest/sha1' |
| 10 |
require 'open3' |
| 11 |
require 'tempfile' |
| 12 |
require "base64" |
| 13 |
|
| 14 |
class MimeParser |
| 15 |
|
| 16 |
LINESEP = "\r\n" |
| 17 |
|
| 18 |
def initialize |
| 19 |
@header_str = "" |
| 20 |
@content_str = "" |
| 21 |
@log = nil |
| 22 |
end |
| 23 |
|
| 24 |
def log( string ) |
| 25 |
if @log |
| 26 |
@log.puts string |
| 27 |
@log.flush |
| 28 |
end |
| 29 |
end |
| 30 |
|
| 31 |
# output data stream for logging |
| 32 |
def setLogStream( stream ) |
| 33 |
@log = stream |
| 34 |
end |
| 35 |
|
| 36 |
# reads complete mime chunk from specified stream |
| 37 |
def read_next_message( stream ) |
| 38 |
|
| 39 |
# stripe initial empty lines |
| 40 |
while( ( line = stream.gets() ) != nil ) |
| 41 |
break if( line.chomp.length != 0 ) |
| 42 |
end |
| 43 |
|
| 44 |
# read message header |
| 45 |
@header_str << line; |
| 46 |
while( ( line = stream.gets() ) != nil ) |
| 47 |
@header_str << line; |
| 48 |
break if( line.chomp.length == 0 ) |
| 49 |
end |
| 50 |
|
| 51 |
# read message content |
| 52 |
while( ( line = stream.gets() ) != nil ) |
| 53 |
@content_str << line; |
| 54 |
break if( line == "." ) |
| 55 |
end |
| 56 |
|
| 57 |
# make sure the message carries original CRLF style |
| 58 |
@header_str.gsub!( LINESEP, "\n" ) |
| 59 |
@header_str.gsub!( "\n", LINESEP ) |
| 60 |
@content_str.gsub!( LINESEP, "\n" ) |
| 61 |
@content_str.gsub!( "\n", LINESEP ) |
| 62 |
|
| 63 |
end |
| 64 |
|
| 65 |
|
| 66 |
def getHeader |
| 67 |
@header_str |
| 68 |
end |
| 69 |
|
| 70 |
|
| 71 |
def getContent |
| 72 |
@content_str |
| 73 |
end |
| 74 |
|
| 75 |
def getMessage |
| 76 |
getHeader + getContent |
| 77 |
end |
| 78 |
|
| 79 |
|
| 80 |
private |
| 81 |
|
| 82 |
# get the range for the complete key-value pair, required to remove key |
| 83 |
def getKeyStringRange( key, string, sepchar ) |
| 84 |
keystring = key+sepchar |
| 85 |
if string.match(/^#{keystring}/i) == nil |
| 86 |
return |
| 87 |
end |
| 88 |
start_index = string.index(/^#{keystring}/i) |
| 89 |
cont_str = string[start_index, string.length - start_index] |
| 90 |
line_length = cont_str.index(/\r\n[-a-zA-Z0-9_"]/) + 2 # CRLF has to be removed too |
| 91 |
[start_index, line_length] |
| 92 |
end |
| 93 |
|
| 94 |
|
| 95 |
# generates array with start index and the length for the value of the given key value |
| 96 |
# for the given string and key separation character (: or =) |
| 97 |
def getKeyValueRange( key, string, sepchar ) |
| 98 |
|
| 99 |
keystring = key+sepchar |
| 100 |
|
| 101 |
start_index = string.index(/^#{keystring}/i) |
| 102 |
if( start_index != nil ) |
| 103 |
|
| 104 |
start_index += keystring.length |
| 105 |
cont_str = string[start_index, string.length - start_index] |
| 106 |
value_length = cont_str.index(/\r\n[-a-zA-Z0-9_"]/) |
| 107 |
if( value_length == nil ) |
| 108 |
value_length = cont_str.length |
| 109 |
if cont_str[cont_str.length-2,2] == LINESEP |
| 110 |
value_length -= 2 # keep CRLF |
| 111 |
end |
| 112 |
end |
| 113 |
|
| 114 |
if value_length > 0 |
| 115 |
[start_index, value_length] |
| 116 |
else |
| 117 |
nil |
| 118 |
end |
| 119 |
|
| 120 |
else |
| 121 |
nil |
| 122 |
end |
| 123 |
end |
| 124 |
|
| 125 |
|
| 126 |
public |
| 127 |
|
| 128 |
# retrieves value for given key |
| 129 |
def getValueForKey( key, string = @header_str ) |
| 130 |
value_range = getKeyValueRange( key, string, ":" ) |
| 131 |
if value_range != nil |
| 132 |
string[ *value_range ].chomp |
| 133 |
else |
| 134 |
"" |
| 135 |
end |
| 136 |
end |
| 137 |
|
| 138 |
|
| 139 |
# assigns a value to a given key |
| 140 |
def setValueForKey( key, value, string = @header_str ) |
| 141 |
if value.strip.length == 0 |
| 142 |
return |
| 143 |
end |
| 144 |
|
| 145 |
value_range = getKeyValueRange( key, string, ":" ) |
| 146 |
if value_range != nil |
| 147 |
string[ *value_range ] = " "+value |
| 148 |
else |
| 149 |
string.sub!( LINESEP+LINESEP, LINESEP + key+": "+value + LINESEP+LINESEP ) |
| 150 |
end |
| 151 |
end |
| 152 |
|
| 153 |
|
| 154 |
# remove a specific key |
| 155 |
def removeKey( key, string = @header_str ) |
| 156 |
value_range = getKeyStringRange( key, string, ":" ) |
| 157 |
string[ *value_range ] = "" if value_range != nil |
| 158 |
end |
| 159 |
|
| 160 |
|
| 161 |
# retrieves value for given subkey, find corresponding |
| 162 |
# main key with getValueforKey value first |
| 163 |
def getValueForSubKey( key, string ) |
| 164 |
keystring = key+"=" |
| 165 |
/(#{keystring})(.+)(;|#{LINESEP})/mi =~ ( string+";" ) |
| 166 |
if Regexp.last_match(2) != nil |
| 167 |
Regexp.last_match(2).chomp |
| 168 |
else |
| 169 |
"" |
| 170 |
end |
| 171 |
end |
| 172 |
|
| 173 |
|
| 174 |
# assigns a value to a given sub key, similar to getValueForSubKey |
| 175 |
def setValueForSubKey( key, value, string ) |
| 176 |
if value.strip.length == 0 |
| 177 |
return |
| 178 |
end |
| 179 |
|
| 180 |
keystring = key+"=" |
| 181 |
/(#{keystring})(.+)(;|#{LINESEP})/mi =~ ( string+";" ) |
| 182 |
offset = Regexp.last_match.offset(2) |
| 183 |
value_range = [offset[0], offset[1]-offset[0]] |
| 184 |
|
| 185 |
if value_range != nil |
| 186 |
string[ *value_range ] = value |
| 187 |
else |
| 188 |
string << "; "+key+"=\""+value+"\"" |
| 189 |
end |
| 190 |
end |
| 191 |
|
| 192 |
|
| 193 |
# retrieves value for Content-Transfer-Encoding |
| 194 |
def getContentTransferEncoding() |
| 195 |
getValueForKey("Content-Transfer-Encoding") |
| 196 |
end |
| 197 |
|
| 198 |
|
| 199 |
# sets value for Content-Transfer-Encoding |
| 200 |
def setContentTransferEncoding( value ) |
| 201 |
setValueForKey( "Content-Transfer-Encoding", value ) |
| 202 |
end |
| 203 |
|
| 204 |
|
| 205 |
# retrieves value for Content-Type |
| 206 |
def getContentType() |
| 207 |
getValueForKey("Content-Type") |
| 208 |
end |
| 209 |
|
| 210 |
|
| 211 |
# sets value for Content-Type |
| 212 |
def setContentType( value ) |
| 213 |
setValueForKey( "Content-Type", value ) |
| 214 |
end |
| 215 |
|
| 216 |
|
| 217 |
# get sender (from) |
| 218 |
def getSender() |
| 219 |
getValueForKey("From") |
| 220 |
end |
| 221 |
|
| 222 |
|
| 223 |
# get to recipient |
| 224 |
def getToRecipients() |
| 225 |
getValueForKey("To") |
| 226 |
end |
| 227 |
|
| 228 |
|
| 229 |
# get CC recipient |
| 230 |
def getCCRecipients() |
| 231 |
getValueForKey("Cc") |
| 232 |
end |
| 233 |
|
| 234 |
|
| 235 |
# get BCC recipient |
| 236 |
def getBCCRecipients() |
| 237 |
getValueForKey("Bcc") |
| 238 |
end |
| 239 |
|
| 240 |
|
| 241 |
# get all recipients() |
| 242 |
def getAllRecipients() |
| 243 |
getToRecipients + getCCRecipients + getBCCRecipients |
| 244 |
end |
| 245 |
|
| 246 |
|
| 247 |
# get all recipients as array of strings |
| 248 |
def getAllRecipientsAsArray() |
| 249 |
str = getAllRecipients |
| 250 |
list = [] |
| 251 |
|
| 252 |
while ( /[\.A-Za-z_-]+@[\.A-Za-z_-]+/ =~ str ) != nil |
| 253 |
range = Regexp.last_match.offset(0) |
| 254 |
len = range[1]-range[0] |
| 255 |
list << str[range[0], len] |
| 256 |
str[range[0]-1, len+1] = "" |
| 257 |
end |
| 258 |
|
| 259 |
list |
| 260 |
end |
| 261 |
|
| 262 |
|
| 263 |
# returns the boundary string |
| 264 |
def getBoundary() |
| 265 |
cntType = getContentType() |
| 266 |
boundary = getValueForSubKey( "boundary", cntType ) |
| 267 |
|
| 268 |
if boundary == nil || boundary == "" |
| 269 |
return nil # no boundaries |
| 270 |
else |
| 271 |
"--"+boundary.gsub("\"","").strip |
| 272 |
end |
| 273 |
|
| 274 |
end |
| 275 |
|
| 276 |
|
| 277 |
# returns all array with ranges for all sections |
| 278 |
# or main content section if no boundaries are defined |
| 279 |
def getSectionRangeArray() |
| 280 |
# get boundary |
| 281 |
boundary = getBoundary() |
| 282 |
if boundary == nil |
| 283 |
return [ (0..getContent.length) ] # no boundaries, return main section |
| 284 |
end |
| 285 |
|
| 286 |
next_start_idx = 0 |
| 287 |
range_array = [ (next_start_idx..next_start_idx) ] |
| 288 |
|
| 289 |
content = getContent |
| 290 |
while /^#{boundary}(\r\n|\n|--)/ =~ content[next_start_idx..content.length] |
| 291 |
next_start_idx = next_start_idx + Regexp.last_match.offset(1)[1] |
| 292 |
length = Regexp.last_match.offset(0)[0] |
| 293 |
|
| 294 |
range_array[-1] = (range_array[-1].min ... range_array[-1].min + length) |
| 295 |
range_array << (next_start_idx .. next_start_idx) |
| 296 |
end |
| 297 |
|
| 298 |
range_array[1...-1] |
| 299 |
end |
| 300 |
|
| 301 |
|
| 302 |
# adds a footer text to each section |
| 303 |
def add_footer_to_each_section( footer_text) |
| 304 |
|
| 305 |
footer_html_text = footer_text. |
| 306 |
gsub("ä", "ä" ). |
| 307 |
gsub("ö", "ö" ). |
| 308 |
gsub("ü", "ü" ). |
| 309 |
gsub("Ä", "Ä"). |
| 310 |
gsub("Ö", "Ö"). |
| 311 |
gsub("Ü", "Ü"). |
| 312 |
gsub("ß", "ß" ). |
| 313 |
gsub("\n", "<br>\n") |
| 314 |
|
| 315 |
footer_7bit_text = footer_text. |
| 316 |
gsub("ä", "ae" ). |
| 317 |
gsub("ö", "oe" ). |
| 318 |
gsub("ü", "ue" ). |
| 319 |
gsub("Ä", "Ae"). |
| 320 |
gsub("Ö", "OE"). |
| 321 |
gsub("Ü", "UE"). |
| 322 |
gsub("ß", "ss" ) |
| 323 |
|
| 324 |
newContent = "" |
| 325 |
boundary = getBoundary() |
| 326 |
boundary = "" if !boundary |
| 327 |
|
| 328 |
rangeArray = getSectionRangeArray |
| 329 |
rangeArray.each do |secrange| |
| 330 |
|
| 331 |
# for each MIME section extract header and content |
| 332 |
section = @content_str[secrange] |
| 333 |
/^\r\n|\n$/ =~ section |
| 334 |
header = section[ 0 .. Regexp.last_match.begin(0) - $~.length ] |
| 335 |
content = section[ Regexp.last_match.offset(0)[1]..section.length ] |
| 336 |
isBase64 = getValueForKey( "Content-Transfer-Encoding", header ).match(/base64/i) |
| 337 |
is7Bit = getValueForKey( "Content-Transfer-Encoding", header ).match(/7bit/i) |
| 338 |
isUTF8 = getValueForKey( "Content-Type", header ).match(/utf-8/i) |
| 339 |
isTextHtml = getValueForKey( "Content-Type", header ).match(/text\/html/i) |
| 340 |
isTextPlain = getValueForKey( "Content-Type", header ).match(/text\/plain/i) |
| 341 |
|
| 342 |
# decode content if it is base64 |
| 343 |
content = Base64.decode64(content) if isBase64 |
| 344 |
|
| 345 |
# add footer message |
| 346 |
if isTextHtml |
| 347 |
# add footer for html |
| 348 |
if( content.match(/<\/body>/) ) |
| 349 |
content.sub!("</body>", "<hr/>" + footer_html_text + "<hr/></body>") |
| 350 |
else |
| 351 |
content << "<hr/>" + footer_html_text + "<hr/>\r\n" |
| 352 |
end |
| 353 |
|
| 354 |
else isTextPlain |
| 355 |
# add footer for plain text |
| 356 |
content << "\r\n" + |
| 357 |
"________________________________________________________________________________\n" + |
| 358 |
( if !isUTF8 then footer_7bit_text else footer_text end ) + "\r\n" |
| 359 |
end |
| 360 |
|
| 361 |
# encode to bas64 again if it was encoded |
| 362 |
content = Base64.encode64(content) if isBase64 |
| 363 |
|
| 364 |
# and add section to new composed message |
| 365 |
newContent << boundary + "\r\n" + header + "\r\n" + content |
| 366 |
|
| 367 |
end # rangeArray.each do |
| 368 |
|
| 369 |
newContent << boundary + "--\r\n" if rangeArray.length > 1 |
| 370 |
@content_str = newContent |
| 371 |
end |
| 372 |
|
| 373 |
end |
| 374 |
|
| 375 |
|
| 376 |
|
| 377 |
class GpgMime < MimeParser |
| 378 |
|
| 379 |
# find gpg command path |
| 380 |
def initialize |
| 381 |
super |
| 382 |
path_array = ["/usr/bin/", "/usr/local/bin/", "/opt/bin/", "/opt/local/bin/"] |
| 383 |
path_array.each do |e| |
| 384 |
fullname = e+"gpg" |
| 385 |
if File.exist?(fullname) |
| 386 |
@gpgCmd = fullname |
| 387 |
break |
| 388 |
end |
| 389 |
end |
| 390 |
end |
| 391 |
|
| 392 |
|
| 393 |
private |
| 394 |
|
| 395 |
TMPDIR = "/tmp" |
| 396 |
@gpgCmd = "gpg" |
| 397 |
GPG_STDOUT_FILE = "gpgstdout" |
| 398 |
GPG_STDIN_FILE = "gpgstdin" |
| 399 |
GPG_STDERR_FILE = "gpgstderr" |
| 400 |
|
| 401 |
|
| 402 |
# retrieves attachment based on type, sufficient for the given PGP/Mime |
| 403 |
# application where we do have only two attachemts which can be distinguished |
| 404 |
# in this way |
| 405 |
def getSubContentOfType( type, boundary ) |
| 406 |
if( ( /(Content-Type:)(.*?)(#{type})/i =~ @content_str ) == nil ) |
| 407 |
return nil |
| 408 |
end |
| 409 |
|
| 410 |
att_beg = Regexp.last_match.offset(0)[1] |
| 411 |
att_end = att_beg + @content_str[ att_beg, @content_str.length - att_beg ].match( boundary ).offset(0)[0] |
| 412 |
|
| 413 |
att_str = @content_str[ att_beg, att_end - att_beg] |
| 414 |
|
| 415 |
att_str.match(LINESEP+LINESEP) |
| 416 |
if Regexp.last_match != nil |
| 417 |
cont_beg_index = Regexp.last_match.offset(0)[1] |
| 418 |
else |
| 419 |
# some GPG/MIME engines unfortunately encrypt obviously with CR instead of CRLF |
| 420 |
att_str.match("\n\n") |
| 421 |
if Regexp.last_match != nil |
| 422 |
cont_beg_index = Regexp.last_match.offset(0)[1] |
| 423 |
else |
| 424 |
cont_beg_index = 0 |
| 425 |
end |
| 426 |
end |
| 427 |
|
| 428 |
att_str[ cont_beg_index, att_str.length - cont_beg_index] |
| 429 |
end |
| 430 |
|
| 431 |
|
| 432 |
# adds signature information of string to the bottom of |
| 433 |
# a MIME message |
| 434 |
def add_signature( string ) |
| 435 |
add_footer_to_each_section( string ) |
| 436 |
end |
| 437 |
|
| 438 |
|
| 439 |
# invokes gpg with optionstring and passphrase if required |
| 440 |
# we use file I/O as workaround for ruby broken stream error |
| 441 |
# when writing larger amounts of data to process pipes |
| 442 |
def gpg( recipient_array, inputstr, optionstr, passphrase=nil ) |
| 443 |
|
| 444 |
gpg_result_code = 0 |
| 445 |
outputstr = "" |
| 446 |
messagestr = "" |
| 447 |
|
| 448 |
# generate message options for gpg |
| 449 |
recipient_str = "" |
| 450 |
recipient_array.each { |address| recipient_str << "-r "+address+" " } |
| 451 |
|
| 452 |
# gpg result code needs to be written to a file |
| 453 |
# since popen3 does not support its transmission |
| 454 |
resultCodeFile = Tempfile.new("gpg_result_code") |
| 455 |
resultCodeFileName = resultCodeFile.path |
| 456 |
resultCodeFile.close |
| 457 |
|
| 458 |
# open pipe for passphrase |
| 459 |
pp_read, pp_write = IO.pipe |
| 460 |
if passphrase != nil |
| 461 |
pp_write.puts( passphrase ) |
| 462 |
pp_option = "--passphrase-fd #{pp_read.fileno}" |
| 463 |
else |
| 464 |
pp_option = " " |
| 465 |
end |
| 466 |
|
| 467 |
command = @gpgCmd + " " + optionstr + " " + pp_option + " " + recipient_str + "; echo $? > #{resultCodeFileName}" |
| 468 |
log "GPG-Command: " + command |
| 469 |
Open3.popen3(command) do |stdin, stdout, stderr| |
| 470 |
Thread.new { |
| 471 |
stdin.write( inputstr ) |
| 472 |
stdin.close_write |
| 473 |
} |
| 474 |
|
| 475 |
outputstr = stdout.read |
| 476 |
messagestr = stderr.read |
| 477 |
end |
| 478 |
|
| 479 |
pp_write.close |
| 480 |
pp_read.close |
| 481 |
File.open(resultCodeFileName,"r") { |f| gpg_result_code = f.read } |
| 482 |
File.unlink( resultCodeFileName ) |
| 483 |
|
| 484 |
# result array |
| 485 |
[outputstr, messagestr, gpg_result_code.to_i] |
| 486 |
end |
| 487 |
|
| 488 |
|
| 489 |
|
| 490 |
|
| 491 |
public |
| 492 |
|
| 493 |
|
| 494 |
# checks whether given passphrase is correct |
| 495 |
def check_passphrase( passphrase ) |
| 496 |
signature, gpgmsg, gpg_result_code = |
| 497 |
gpg( [], "to_sign", "--batch -o - -abs", passphrase ) |
| 498 |
|
| 499 |
# process signature |
| 500 |
if( gpg_result_code == 0 ) |
| 501 |
true |
| 502 |
else |
| 503 |
false |
| 504 |
end |
| 505 |
|
| 506 |
end |
| 507 |
|
| 508 |
|
| 509 |
# encrypts and signs class internals or delivers error message in case of errors |
| 510 |
# if passphrase is provided, the message is signed with the default key |
| 511 |
def encrypt_and_sign( passphrase, recipient_array = nil ) |
| 512 |
gpgmsg = "" |
| 513 |
boundary = "--" + Digest::SHA1.hexdigest( getHeader ) |
| 514 |
|
| 515 |
# construct message content to encrypt |
| 516 |
to_encrypt = "Content-Type: "+ getContentType + LINESEP + |
| 517 |
"Content-Transfer-Encoding: " + getContentTransferEncoding + LINESEP + LINESEP + |
| 518 |
@content_str |
| 519 |
|
| 520 |
# if recipient array is not given, use all recipients |
| 521 |
# which are specified within MIME-header |
| 522 |
if( recipient_array == nil ) |
| 523 |
recipient_array = getAllRecipientsAsArray() |
| 524 |
end |
| 525 |
|
| 526 |
# invoke gpg subprocess |
| 527 |
optionstr = "--batch -ea" |
| 528 |
optionstr += "s" if passphrase != nil |
| 529 |
encrypted, gpgmsg, gpg_result_code = |
| 530 |
gpg( recipient_array, to_encrypt, optionstr, passphrase ) |
| 531 |
|
| 532 |
# handle encrypted result |
| 533 |
if( gpg_result_code == 0 ) |
| 534 |
# Success, store encryption result to class internals |
| 535 |
|
| 536 |
@content_str = |
| 537 |
"This is an OpenPGP/MIME encrypted message (RFC 2440 and 3156)"+LINESEP+ |
| 538 |
"--#{boundary}"+LINESEP+ |
| 539 |
"Content-Type: application/pgp-encrypted"+LINESEP+ |
| 540 |
"Content-Description: PGP/MIME version identification"+LINESEP+ |
| 541 |
LINESEP+ |
| 542 |
"Version: 1"+LINESEP+ |
| 543 |
LINESEP+ |
| 544 |
"--#{boundary}"+LINESEP+ |
| 545 |
"Content-Type: application/octet-stream; name=\"encrypted.asc\""+LINESEP+ |
| 546 |
"Content-Description: OpenPGP encrypted message"+LINESEP+ |
| 547 |
"Content-Disposition: inline; filename=\"encrypted.asc\"" +LINESEP+ |
| 548 |
LINESEP+ |
| 549 |
encrypted + LINESEP+ |
| 550 |
"--#{boundary}--" + LINESEP |
| 551 |
|
| 552 |
setContentType( "multipart/encrypted;" + LINESEP + |
| 553 |
"\tprotocol=\"application/pgp-encrypted\";" + LINESEP + |
| 554 |
"\tboundary=\""+boundary+"\"") |
| 555 |
|
| 556 |
end |
| 557 |
|
| 558 |
# deliver gpg error code to invoker if any |
| 559 |
if gpg_result_code != 0 |
| 560 |
gpgmsg |
| 561 |
else |
| 562 |
"" |
| 563 |
end |
| 564 |
|
| 565 |
end |
| 566 |
|
| 567 |
|
| 568 |
# encrypts class internals or delivers error message in case of errors |
| 569 |
# if passphrase is provided, the message is signed with the default key |
| 570 |
def encrypt( passphrase = nil, recipient_array = nil ) |
| 571 |
encrypt_and_sign( nil, recipient_array ) |
| 572 |
end |
| 573 |
|
| 574 |
|
| 575 |
# generates clear text signature for class internals or delivers error message |
| 576 |
# in case of errors |
| 577 |
def clearsign( passphrase ) |
| 578 |
gpgmsg = "" |
| 579 |
signature = "" |
| 580 |
|
| 581 |
boundary = "--" + Digest::SHA1.hexdigest( getHeader ) |
| 582 |
|
| 583 |
# determine which part of the message to be signed |
| 584 |
to_sign = "Content-Type: "+ getContentType + LINESEP |
| 585 |
if getContentTransferEncoding.length > 0 |
| 586 |
to_sign << "Content-Transfer-Encoding: " + getContentTransferEncoding + LINESEP + LINESEP |
| 587 |
end |
| 588 |
to_sign << @content_str |
| 589 |
|
| 590 |
|
| 591 |
# make sure everything is encoded DOS like (CR+LF) |
| 592 |
to_sign.gsub!( /\r\n/, "\n" ) # in case we have already DOS encodings we code back |
| 593 |
|
| 594 |
# remove initial and trailing blanks |
| 595 |
to_sign.gsub!(/^[ ]+/,"") |
| 596 |
to_sign.gsub!(/[ ]+$/,"") |
| 597 |
|
| 598 |
# signed content must end with a CR+LF sequence, so make sure |
| 599 |
# that there is only one CR first |
| 600 |
i = to_sign.length - 1 |
| 601 |
while( to_sign[i] == ?\n && i > 0 ) do i-=1; end |
| 602 |
to_sign = to_sign[0, i+1] + "\n" |
| 603 |
|
| 604 |
# and replace all CR's with CR+LF |
| 605 |
to_sign.gsub!( /\n/, "\r\n" ) # and forth |
| 606 |
|
| 607 |
|
| 608 |
# log file for debugging |
| 609 |
=begin |
| 610 |
fp = File.new("to_sign.eml", "w") |
| 611 |
fp.write(to_sign) |
| 612 |
fp.close |
| 613 |
=end |
| 614 |
|
| 615 |
# invoke gpg subprocess |
| 616 |
signature, gpgmsg, gpg_result_code = |
| 617 |
gpg( [], to_sign, "--batch -o - -abs", passphrase ) |
| 618 |
|
| 619 |
# process signature |
| 620 |
if( gpg_result_code == 0 ) |
| 621 |
# Success, store encryption result to class internals |
| 622 |
|
| 623 |
@content_str = |
| 624 |
"This is an OpenPGP/MIME signed message (RFC 2440 and 3156)"+LINESEP+ |
| 625 |
"--#{boundary}"+LINESEP+ |
| 626 |
to_sign |
| 627 |
|
| 628 |
setContentType( "multipart/signed;" + LINESEP + |
| 629 |
"\tprotocol=\"application/pgp-signature\"; micalg=pgp-sha1;" + LINESEP + |
| 630 |
"\tboundary=\""+boundary+"\"") |
| 631 |
|
| 632 |
@content_str << LINESEP + "--#{boundary}" + LINESEP + |
| 633 |
"Content-Type: application/pgp-signature; name=\"signature.asc\"" + LINESEP + |
| 634 |
"Content-Description: OpenPGP digital signature" + LINESEP + |
| 635 |
"Content-Disposition: attachment; filename=\"signature.asc\"" + LINESEP + |
| 636 |
LINESEP + |
| 637 |
signature + LINESEP + |
| 638 |
"--#{boundary}--" + LINESEP + LINESEP |
| 639 |
end |
| 640 |
|
| 641 |
# deliver gpg error code to invoker if any |
| 642 |
if gpg_result_code != 0 |
| 643 |
gpgmsg |
| 644 |
else |
| 645 |
"" |
| 646 |
end |
| 647 |
|
| 648 |
end |
| 649 |
|
| 650 |
|
| 651 |
private |
| 652 |
|
| 653 |
# decryptes GPG/inline content |
| 654 |
def decrypt( passphrase ) |
| 655 |
|
| 656 |
decrypted = "" |
| 657 |
gpgmsg = "" |
| 658 |
|
| 659 |
# check mime type and get boundary |
| 660 |
cntType = getContentType() |
| 661 |
|
| 662 |
if cntType.match("application\/pgp-encrypted") == nil |
| 663 |
return "input stream with wrong content-type, must be pgp-encrypted!" |
| 664 |
end |
| 665 |
boundary = getValueForSubKey( "boundary", cntType ) |
| 666 |
/(")(.*?)(")/=~boundary |
| 667 |
boundary = "--"+Regexp.last_match(2) |
| 668 |
|
| 669 |
# check PGP/Mime Version ( normally this is in the first attachment ) |
| 670 |
version_str = getSubContentOfType( "application\/pgp-encrypted", boundary ) |
| 671 |
if version_str==nil || version_str.match("Version: 1") == nil |
| 672 |
return "only version 1 for application/pgp-encrypted mime type supported!" |
| 673 |
end |
| 674 |
|
| 675 |
# extract content |
| 676 |
pgp_str = getSubContentOfType( "application\/octet-stream", boundary ) |
| 677 |
if( pgp_str == nil ) |
| 678 |
return "missing section application/octed stream in input stream!" |
| 679 |
end |
| 680 |
|
| 681 |
# decode message |
| 682 |
# invoke gpg subprocess |
| 683 |
decrypted, gpgmsg, gpg_result_code = |
| 684 |
gpg( [], pgp_str, "--batch -d ", passphrase ) |
| 685 |
|
| 686 |
# RFC 822 requires originally CRLF, but some mua handle it different |
| 687 |
# split encrypted content in original header and content information |
| 688 |
splitmatch = decrypted.match( LINESEP+LINESEP ) |
| 689 |
splitmatch = decrypted.match( "\n\n" ) if splitmatch == nil |
| 690 |
if splitmatch == nil |
| 691 |
return "decrypted content could not be decoded or has no header!" |
| 692 |
end |
| 693 |
|
| 694 |
decrypted_header_end = splitmatch.offset(0)[0] |
| 695 |
decrypted_content_beg = splitmatch.offset(0)[1] |
| 696 |
decrypted_header = decrypted[0, decrypted_header_end ] |
| 697 |
decrypted_content = decrypted[decrypted_content_beg, decrypted.length - decrypted_content_beg] |
| 698 |
|
| 699 |
# assign original encoding to header |
| 700 |
setContentType( getValueForKey( "Content-Type", decrypted_header ) ) |
| 701 |
setContentTransferEncoding( getValueForKey( "Content-Transfer-Encoding", decrypted_header ) ) |
| 702 |
|
| 703 |
# exchange content and attach gpg signature info at the end of the message |
| 704 |
@content_str = decrypted_content + LINESEP |
| 705 |
|
| 706 |
# deliver gpg error code to invoker if any |
| 707 |
if gpg_result_code != 0 |
| 708 |
gpgmsg |
| 709 |
else |
| 710 |
if getContentType().match( "application\/pgp-signature" ) |
| 711 |
check_clearsig |
| 712 |
else |
| 713 |
add_signature( gpgmsg ) |
| 714 |
"" |
| 715 |
end |
| 716 |
end |
| 717 |
|
| 718 |
end |
| 719 |
|
| 720 |
|
| 721 |
# checks clear text signature |
| 722 |
def check_clearsig |
| 723 |
|
| 724 |
# check mime type and get boundary |
| 725 |
cntType = getContentType() |
| 726 |
if cntType.match("application\/pgp-signature") == nil |
| 727 |
return "input stream with wrong content-type, must be pgp-signature!" |
| 728 |
end |
| 729 |
boundary = getValueForSubKey( "boundary", cntType ) |
| 730 |
/(")(.*?)(")/=~boundary |
| 731 |
boundary = "--"+Regexp.last_match(2) |
| 732 |
|
| 733 |
# extract signed content which must be the first section |
| 734 |
# some email clients unfortunately use CR instead of CRLF before encrpytion |
| 735 |
if @content_str.match( boundary+LINESEP ) == nil && @content_str.match( boundary+"\n" ) == nil |
| 736 |
return "input stream does not provide specified boundaries!" |
| 737 |
end |
| 738 |
|
| 739 |
signed_content = @content_str[Regexp.last_match.offset(0)[1], @content_str.length] |
| 740 |
|
| 741 |
if signed_content.match( LINESEP+boundary ) == nil && signed_content.match( "\n"+boundary ) == nil |
| 742 |
return "input stream does not provide specified boundaries!" |
| 743 |
end |
| 744 |
|
| 745 |
signed_content = $` |
| 746 |
|
| 747 |
# extract key |
| 748 |
sig_str = getSubContentOfType( "application\/pgp-signature", boundary ) |
| 749 |
if( sig_str == nil ) |
| 750 |
return "missing section application/pgp-signature in input stream!" |
| 751 |
end |
| 752 |
|
| 753 |
# and write it to temporary file |
| 754 |
sigfilename = File.join(TMPDIR, "tmp.sig") |
| 755 |
File.open(sigfilename, "w") { |sigstream| sigstream.write sig_str } |
| 756 |
|
| 757 |
# check message signature |
| 758 |
# invoke gpg subprocess |
| 759 |
dummy, gpgmsg, gpg_result_code = |
| 760 |
gpg( [], signed_content, "--batch --verify #{sigfilename} -", nil ) |
| 761 |
|
| 762 |
# RFC 822 requires originally CRLF, but some mua handle it different |
| 763 |
# split encrypted content in original header and content information |
| 764 |
splitmatch = signed_content.match( LINESEP+LINESEP ) |
| 765 |
splitmatch = signed_content.match( "\n\n" ) if splitmatch == nil |
| 766 |
if splitmatch == nil |
| 767 |
return "signed content has no encoding header!" |
| 768 |
end |
| 769 |
|
| 770 |
header_end = splitmatch.offset(0)[0] |
| 771 |
content_beg = splitmatch.offset(0)[1] |
| 772 |
header = signed_content[0, header_end ] |
| 773 |
content = signed_content[content_beg, signed_content.length - content_beg] |
| 774 |
|
| 775 |
# assign original encoding to header |
| 776 |
setContentType( getValueForKey( "Content-Type", header ) ) |
| 777 |
setContentTransferEncoding( getValueForKey( "Content-Transfer-Encoding", header ) ) |
| 778 |
|
| 779 |
# exchange content and attach gpg signature info at the end of the message |
| 780 |
@content_str = content |
| 781 |
add_signature( gpgmsg ) |
| 782 |
|
| 783 |
# deliver gpg error code to invoker if any |
| 784 |
if gpg_result_code != 0 |
| 785 |
gpgmsg |
| 786 |
else |
| 787 |
"" |
| 788 |
end |
| 789 |
|
| 790 |
end |
| 791 |
|
| 792 |
|
| 793 |
|
| 794 |
public |
| 795 |
|
| 796 |
# invokes depending of content type decrypt or check_clearsig |
| 797 |
def decode( passphrase ) |
| 798 |
|
| 799 |
contentType = getContentType() |
| 800 |
res = "" |
| 801 |
|
| 802 |
case |
| 803 |
when contentType.match( "application\/pgp-signature" ) |
| 804 |
res = check_clearsig |
| 805 |
|
| 806 |
when contentType.match( "application/pgp-encrypted" ) |
| 807 |
res = decrypt( passphrase ) |
| 808 |
end |
| 809 |
|
| 810 |
return res |
| 811 |
|
| 812 |
end |
| 813 |
|
| 814 |
|
| 815 |
# delivers an array with all email addresses to public keys |
| 816 |
def getPubKeyAddressList |
| 817 |
outputstr, messagestr, gpg_result_code = gpg( [], [], "--batch -k " ) |
| 818 |
|
| 819 |
if gpg_result_code == 0 |
| 820 |
|
| 821 |
list = [] |
| 822 |
|
| 823 |
while ( /[\.A-Za-z_-]+@[\.A-Za-z_-]+/ =~ outputstr ) != nil |
| 824 |
range = Regexp.last_match.offset(0) |
| 825 |
len = range[1]-range[0] |
| 826 |
list << outputstr[range[0], len] |
| 827 |
outputstr[range[0]-1, len+1] = "" |
| 828 |
end |
| 829 |
|
| 830 |
list |
| 831 |
|
| 832 |
else |
| 833 |
[] # error |
| 834 |
end |
| 835 |
|
| 836 |
end |
| 837 |
|
| 838 |
end |
| 839 |
|
| 840 |
__END__ |