This documentation is for Dovecot v2.x, see wiki1 for v1.x documentation.

Attachment 'decrypt.rb'

Download

   1 #!/usr/bin/env ruby
   2 
   3 ## Released to public domain
   4 
   5 require 'openssl'
   6 require 'optparse'
   7 
   8 def read_oid(stream)
   9   ## read length
  10   tmp = stream.read(2)
  11   if tmp[1].ord & 0x80 != 0
  12     # read bit more
  13     tmp = "#{tmp}#{stream.read(1)}"
  14     len = ((tmp[1] & 0x7f) << 8) + tmp[2].ord
  15   else
  16     len = tmp[1].ord
  17   end
  18   tmp = "#{tmp}#{stream.read len}".force_encoding("binary")
  19   OpenSSL::ASN1.decode(tmp)
  20 end
  21 
  22 def get_pubid_priv(key)
  23   pub = key.public_key
  24   grp = key.group
  25   grp.point_conversion_form = :compressed
  26   seq = OpenSSL::ASN1::Sequence.new([
  27          OpenSSL::ASN1::Sequence.new([
  28            OpenSSL::ASN1::ObjectId.new('id-ecPublicKey'),
  29            OpenSSL::ASN1.decode(key.group.to_der),
  30          ]),
  31          OpenSSL::ASN1::BitString.new(pub.to_bn.to_s(2))
  32        ])
  33   OpenSSL::Digest::SHA256.new.digest(seq.to_der.force_encoding("binary"))
  34 end
  35 
  36 options = {input: STDIN, output: STDOUT}
  37 
  38 op = OptionParser.new do |opts|
  39   opts.banner = "Usage: #{$0} -k key -i -f file -w file"
  40 
  41   opts.on("-i","--info", "Show information about file") do |i|
  42     options[:info] = i
  43   end
  44 
  45   opts.on("-k","--key KEY", "Private key to decrypt file") do |k|
  46     options[:key] = OpenSSL::PKey.read(File.open(k))
  47     options[:key_digest] = get_pubid_priv(options[:key])
  48   end
  49 
  50   opts.on("-f", "--file FILE", "File to read instead of stdin") do |f|
  51     options[:input] = File.open(f,"rb")
  52   end
  53 
  54   opts.on("-w","--write FILE", "File to write contents instead of stdout") do |w|
  55     options[:output] = File.open(w,"wb")
  56   end
  57 
  58   opts.on("-h","--help", "Show help") do |h|
  59     puts opts
  60     exit 0
  61   end
  62 
  63 end.parse(ARGV)
  64 
  65 unless options[:key] or options[:info]
  66   exit 0
  67 end
  68 
  69 file = {}
  70 
  71 ## check if we understand this file
  72 unless options[:input].read(9) == "CRYPTED\x03\a"
  73   raise "Not encrypted with dovecot"
  74 end
  75 
  76 options[:input].set_encoding("binary")
  77 
  78 ## read file version
  79 file[:version] = options[:input].read(1).unpack('C').shift
  80 
  81 if file[:version] == 2
  82 
  83   ## read flags
  84   file[:flags] = options[:input].read(4).unpack('I>').shift
  85 
  86   file[:flags_expanded] = []
  87   file[:flags_expanded] << "HMAC integrity" if (file[:flags] & 0x01) == 0x01
  88   file[:flags_expanded] << "AEAD integrity" if (file[:flags] & 0x02) == 0x02
  89   file[:flags_expanded] << "No integrity" if (file[:flags] & 0x04) == 0x04
  90   file[:flags_expanded] = file[:flags_expanded].join " ,"
  91 
  92   ## read header length and specs
  93   file[:hdr_len] = options[:input].read(4).unpack('I>').shift
  94 
  95   file[:cipher] = read_oid(options[:input])
  96   file[:digest] = read_oid(options[:input])
  97 
  98   (file[:rounds], file[:kdlen], nkeys) = options[:input].read(9).unpack('I>I>C')
  99 
 100   ## read all keys
 101   file[:keys] = []
 102   our_key = nil
 103   tlen = 0
 104   while(nkeys>0) do
 105      nkeys = nkeys - 1
 106      key = {}
 107 
 108      ## Unpack key type and key digest
 109      (key[:type],key[:digest]) = options[:input].read(33).unpack('Ca*')
 110 
 111      if key[:type] == 1
 112        key[:type] = "RSA"
 113      elsif key[:type] == 2
 114        key[:type] = "EC"
 115      end
 116 
 117      tlen = tlen + 33
 118      ## read length and data
 119      len = options[:input].read(4).unpack('I>').shift
 120      tlen = tlen + len
 121      key[:peer_key] = options[:input].read(len)
 122      len = options[:input].read(4).unpack('I>').shift
 123      tlen = tlen + len
 124      key[:encrypted] = options[:input].read(len).force_encoding("binary")
 125      len = options[:input].read(4).unpack('I>').shift
 126      tlen = tlen + len
 127      key[:data_digest] = options[:input].read(len)
 128 
 129      our_key = key if key[:digest] == options[:key_digest]
 130 
 131      file[:keys] << key
 132   end
 133 
 134   if options[:input].tell != file[:hdr_len]
 135      our_key = nil
 136      print "Error: header length mismatch"
 137   end
 138 
 139   unless our_key == nil
 140      # decrypt data!
 141 
 142      grp = options[:key].group
 143      grp.point_conversion_form = :compressed
 144      our_key[:ephemeral] = OpenSSL::PKey::EC::Point.new(grp, OpenSSL::BN.new(our_key[:peer_key], 2))
 145 
 146      file[:secret] = options[:key].dh_compute_key(our_key[:ephemeral])
 147 
 148      dk_a = OpenSSL::PKCS5.pbkdf2_hmac(file[:secret], key[:peer_key], file[:rounds], 32+16, OpenSSL::Digest.new(file[:digest].ln))
 149      cipher = OpenSSL::Cipher.new("AES-256-CBC")
 150 
 151      cipher.decrypt
 152      cipher.key = dk_a[0,32]
 153      cipher.iv = dk_a[32,16]
 154 
 155      dk_b = cipher.update key[:encrypted]
 156      dk_b.force_encoding("binary")
 157      dk_b = "#{dk_b}#{cipher.final}"
 158      dk_b.force_encoding("binary")
 159 
 160      file[:temp_key] = dk_a[0,32]
 161      file[:temp_iv] = dk_a[32,16]
 162 
 163      hash = OpenSSL::Digest.new(file[:digest].ln).digest(dk_b)
 164 
 165      (1..2048).each do |i|
 166        d = OpenSSL::Digest.new(file[:digest].ln)
 167        d << hash
 168        d << [i].pack('I>')
 169        hash = d.digest
 170      end
 171 
 172      if hash != our_key[:data_digest]
 173        puts "Decryption error (did not decipher encryption key correctly)"
 174      end
 175 
 176      # now we have keying data
 177      file[:sym_key] = dk_b[0,32]
 178      file[:sym_iv] = dk_b[32,12]
 179      file[:sym_aad] = dk_b[44,16]
 180 
 181      # see if we can decrypt it
 182      cipher = OpenSSL::Cipher.new(file[:cipher].ln)
 183      cipher.decrypt
 184 
 185      cipher.key = file[:sym_key]
 186      cipher.iv = file[:sym_iv]
 187 
 188      # read data
 189      data = options[:input].read
 190 
 191      if options[:input].eof?
 192        file[:sym_tag] = data[data.length-16, 16]
 193        cipher.auth_tag = file[:sym_tag]
 194        data = data[0,data.length-16]
 195        file[:data_size] = data.size
 196      end
 197 
 198      cipher.auth_data = file[:sym_aad]
 199      options[:output].print cipher.update data
 200      options[:output].print cipher.final
 201   end
 202 
 203   if options[:info]
 204     STDERR.puts(<<EOF
 205 Version       : #{file[:version]}
 206 Flags         : #{file[:flags_expanded]}
 207 Header length : #{file[:hdr_len]}
 208 Cipher algo   : #{file[:cipher].ln} (#{file[:cipher].oid})
 209 Digest algo   : #{file[:digest].ln} (#{file[:digest].oid})
 210 
 211 Key derivation
 212   - Rounds    : #{file[:rounds]}
 213 EOF
 214 )
 215   end
 216 
 217   if our_key
 218     STDERR.puts(<<EOF
 219   - Secret    : #{file[:secret].unpack('H*').shift}
 220   - Salt      : #{our_key[:peer_key].unpack('H*').shift}
 221 
 222 Encryption key decryption:
 223   - Encrypted : #{our_key[:encrypted].unpack('H*').shift}
 224   - Key       : #{file[:temp_key].unpack('H*').shift}
 225   - IV        : #{file[:temp_iv].unpack('H*').shift}
 226 
 227 Decryption
 228   - Key       : #{file[:sym_key].unpack('H*').shift}
 229   - IV        : #{file[:sym_iv].unpack('H*').shift}
 230 EOF
 231 )
 232     if (file[:flags] & 0x02) == 0x02
 233       STDERR.puts "  - AAD       : #{file[:sym_aad].unpack('H*').shift}"
 234       STDERR.puts "  - TAG       : #{file[:sym_tag].unpack('H*').shift}"
 235     end
 236   end
 237 
 238   STDERR.puts "\nKey(s) (total: #{file[:keys].count})\n"
 239   file[:keys].each do |key|
 240      STDERR.puts(<<EOF
 241   - Key type  : #{key[:type]}
 242   - Key digest: #{key[:digest].unpack('H*').shift}
 243   - Peer key  : #{key[:peer_key].unpack('H*').shift}
 244   - Encrypted : #{key[:encrypted].unpack('H*').shift}
 245   - Kd hash   : #{key[:data_digest].unpack('H*').shift}
 246 EOF
 247 )
 248   end
 249 elsif file[:version] == 1
 250   # total header length
 251   file[:hdr_len] = options[:input].read(2).unpack('S>').shift + 12
 252   key = {}
 253 
 254   ## Read peer key
 255   len = options[:input].read(2).unpack('S>').shift
 256   key[:peer_key] = options[:input].read(len)
 257   if options[:key]
 258     grp = options[:key].group
 259     grp.point_conversion_form = :compressed
 260     key[:ephemeral] = OpenSSL::PKey::EC::Point.new(grp, OpenSSL::BN.new(key[:peer_key], 2))
 261   end
 262 
 263   ## Read public key ID
 264   len = options[:input].read(2).unpack('S>').shift
 265   key[:digest] = options[:input].read(len)
 266 
 267   ## Read encryption key hash
 268   len = options[:input].read(2).unpack('S>').shift
 269   key[:data_digest] = options[:input].read(len)
 270 
 271   ## Read encrypted encryption key
 272   len = options[:input].read(2).unpack('S>').shift
 273   key[:encrypted] = options[:input].read(len)
 274   file[:keys] = [key]
 275 
 276   ## This should be 0
 277   if options[:input].read(2).unpack('S>').shift != 0
 278     STDERR.puts "Decryption warning: header format mismatch"
 279   end
 280 
 281   ## See if header is consumed
 282   if options[:input].tell != file[:hdr_len]
 283     p options[:input].tell
 284     p file[:hdr_len]
 285     STDERR.puts "Decryption warning: header length mismatch"
 286   end
 287 
 288   # assume it's right key because it's hard to do in ruby
 289   if options[:key]
 290     file[:secret] = options[:key].dh_compute_key(key[:ephemeral])
 291     file[:temp_key] = OpenSSL::Digest::SHA256.digest(file[:secret])
 292 
 293     ## Decrypt encryption key
 294     cipher = OpenSSL::Cipher.new("AES-256-CTR")
 295     cipher.decrypt
 296     cipher.key = file[:temp_key]
 297     cipher.iv = "\x0" * 16
 298     file[:sym_key] = cipher.update key[:encrypted]
 299     file[:sym_key] = "#{file[:sym_key]}#{cipher.final}"
 300 
 301     ## Check it's correct
 302     if key[:data_digest] != OpenSSL::Digest::SHA256.digest(file[:sym_key])
 303       raise "Decryption error: invalid decryption key"
 304     end
 305 
 306     ## Decrypt file
 307     cipher = OpenSSL::Cipher.new("AES-256-CTR")
 308     cipher.decrypt
 309     cipher.key = file[:sym_key]
 310     cipher.iv = "\x0" * 16
 311 
 312     options[:output].print cipher.update options[:input].read
 313     options[:output].print cipher.final
 314   end
 315 
 316   if options[:info]
 317     STDERR.puts(<<EOF
 318 Version       : #{file[:version]}
 319 Header length : #{file[:hdr_len]}
 320 Cipher algo   : aes-256-ctr
 321 Digest algo   : sha256
 322 
 323 EOF
 324 )
 325   end
 326 
 327   if options[:key]
 328     STDERR.puts(<<EOF
 329 Encryption key decryption:
 330   - Secret    : #{file[:secret].unpack('H*').shift}
 331   - Encrypted : #{key[:encrypted].unpack('H*').shift}
 332   - Key       : #{file[:temp_key].unpack('H*').shift}
 333   - IV        : 00000000000000000000000000000000
 334 
 335 Decryption
 336   - Key       : #{file[:sym_key].unpack('H*').shift}
 337   - IV        : 00000000000000000000000000000000
 338 EOF
 339 )
 340   end
 341 
 342   STDERR.puts "\nKey(s) (total: #{file[:keys].count})\n"
 343   file[:keys].each do |key|
 344      STDERR.puts(<<EOF
 345   - Key type  : EC
 346   - Key digest: #{key[:digest].unpack('H*').shift}
 347   - Peer key  : #{key[:peer_key].unpack('H*').shift}
 348   - Encrypted : #{key[:encrypted].unpack('H*').shift}
 349   - Kd hash   : #{key[:data_digest].unpack('H*').shift}
 350 EOF
 351 )
 352   end
 353 else
 354   raise "Unsupported version #{file[:version]}"
 355 end

New Attachment

File to upload
Rename to
Overwrite existing attachment of same name
What do you do to prevent spam?

Attached Files

To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.
  • [get | view] (2017-03-28 09:31:54, 9.6 KB) [[attachment:decrypt.rb]]
 All files | Selected Files: delete move to page copy to page