Class: RepoInternal::MyAES

Inherits:
Object
  • Object
show all
Defined in:
lib/lenc/aes.rb

Overview

Wrapper for OpenSSL AES cipher.

Usage for encryption:
----------------------------------------------------
original = "..."          # bytes to encrypt (string)

key = "xxxx..."           # encryption key (string of size 8..56)

en = MyAES.new(true, key) # construct an encryptor    

en.finish(original)       # add data to encrypt          

encrypted = en.flush()    # get encrypted bytes (string)
----------------------------------------------------

Usage for decryption:
----------------------------------------------------
encrypted = "..."         # bytes to decrypt 

key = "xxxx...."    

de = MyAES.new(false, key)  # construct a decryptor

de.finish(original)       # add data to decrypt

decrypted = de.flush()    # get decrypted bytes

----------------------------------------------------

Use of nonces:
--------------
The above encryption example generates a new (hopefully unique) 'nonce' 
which is an added security feature.  It uses the system clock to do this.
This means the same file will produce different encrypted byte streams on 
repeated encryption attempts, which may be undesirable.  A fixed nonce
(for a particular input file) can be specified as an additional input:

nonce = "nnnn.."         # only the first 8 bytes are used
en = MyAES.new(true, key, nonce)

The nonce, whether explicitly given or randomly generated, is added to the
encrypted stream; hence it need not be specified when decrypting.

Stream mode:
------------
When processing large files, you may want to do them a chunk at a time.
Here's an example of encrypting using stream mode (decrypting is similar):

en = MyAES.new(true, key)    

s = {size of input file}
n = 0
while n < s  
  c = [5000, s - n].min
  en.add( {bytes n..n+c-1 from the input file} )
  r = en.flush()
  {append bytes r to output file}
end

en.finish()
r = en.flush()
{append bytes r to output file}

----------------------------------------------------

Format of encrypted data:

[8] nonce (only the first 8 bytes of the nonce are actually used)

Followed by one or more encrypted chunks of length [k], where k is 65536, unless it's the last
chunk in the file, in which case it must be a multiple of 16.

The first bytes of each decrypted chunk is a header:
  [7]  zeros
  [1]  number of padding bytes present at end of block

For example, suppose a file of 71980 'source' bytes has been encrypted.  The encrypted file will contain:
  [8] nonce
  [65536] first chunk, consisting of
     [7] zeros
     [1] zero, since this chunk needed no padding
     [65528] 65528 encrypted source bytes
  [6464]  second chunk, consisting of
     [7] zeros
     [1] 4, indicating 4 padding bytes
     [6456] 6452 encrypted source bytes plus 4 padding bytes 

  Observe that 65528 + 6452 = 71980.

The purpose of the [7] zeros in the (decrypted) chunk header are to indicate
whether decryption was successful (e.g., if the password was correct).  The assumption
is that an incorrect password will generate 7 zeros in these locations with extremely low probability.

The byte used as a padding byte is 254.

If a file has length zero, then when encrypted, it will have the following structure:
  [8] nonce
  [16] chunk:
    [7] zeros
    [1] 8, indicating 8 padding bytes
    [8] 0 encrypted source bytes plus 8 padding bytes

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#prefix_modeObject

Returns the value of attribute prefix_mode.



145
146
147
# File 'lib/lenc/aes.rb', line 145

def prefix_mode
  @prefix_mode
end

Class Method Details

.is_file_encrypted(key, path) ⇒ Object

Determines if a file is an encrypted file for the given password, and the file is of the expected length.

Parameters:

  • key

    password to use (string, or array of bytes)

  • path

    path to file

Returns:

  • true iff the start of the file seems to decrypt correctly



489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
# File 'lib/lenc/aes.rb', line 489

def self.is_file_encrypted(key, path) 
  
  db = warndb 0
  
  !db || pr("is_file_encrypted '#{path}'?\n")
#    key = str_to_bytes(key)
  
  if not File.file?(path) 
    !db || pr(" not a file\n")
    return false
  end
  
  lnth = File.size(path)
  minSize = NONCE_SIZE_SMALL + AES_BLOCK_SIZE
  !db || pr(" file size=#{lnth}, minSize=#{minSize}\n")
  if lnth < minSize or ((lnth - minSize) % AES_BLOCK_SIZE) != 0
    !db || pr(" length not appropriate\n")
    return false
  end
  
  if false
    warn("using full size of file")
    minSize = lnth
  end
  
  f = File.open(path,"rb")
  s = f.read(minSize)
  ret = is_string_encrypted(key, s)
  !db || pr(" is_string_encrypted returning #{ret}\n")
  ret
end

.is_string_encrypted(key, test_str) ⇒ Object

Determines if a string is the start of an encrypted sequence

Returns true iff the start of the string seems to decrypt correctly for the given password

Parameters:

  • key

    password to use (string)

  • test_str

    the string to test



440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
# File 'lib/lenc/aes.rb', line 440

def self.is_string_encrypted(key, test_str) 
  db = warndb  0
  
  !db || hex_dump(test_str, "is_string_encrypted?")
  
  simple_str(test_str)
  
  lnth = test_str.size
  lnth -= NONCE_SIZE_SMALL
  if lnth < AES_BLOCK_SIZE || lnth % AES_BLOCK_SIZE != 0
    !db || pr("  bad # bytes\n")
    return false
  end
  
  hdr_size = AES_BLOCK_SIZE
  
  # This method is failing, I suspect because with the mode of AES we're using (CRC?) we can't
  # decrypt only a single block, and must instead decrypt a complete chunk.
  
  # No, now I think it's interpreting a bad 'padding' value (due to only decrypting partially)
  # as indication of bad decryption
  
  if false
    warn("using full chunk size")
    hdr_size = [lnth,CHUNK_SIZE_ENCR].min
  end
  
  begin
      de = MyAES.new(false, key)  
      
      # Put this decryptor into prefix mode, so that we are only interested
      # in whether the header verifies correctly
      de.prefix_mode = true
           
      de.finish(test_str[0...hdr_size + NONCE_SIZE_SMALL])
      de.flush()
  rescue LEnc::DecryptionError => e
    !db || pr(" (caught DecryptionError #{e})\n")
    return false
  end
    
  true
end

Instance Method Details

#add(data) ⇒ Object

Process additional input bytes, encrypting (or decrypting) its contents

Parameters:

  • data

    string containing input bytes

Raises:



350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# File 'lib/lenc/aes.rb', line 350

def add(data)
   
  raise IllegalStateException if @finished 
  
  simple_str(data)
  
  @inputBuffer << data #.concat(data)

  while true 

    if not @encrypting 
      
      # Extract nonce if we're waiting for it and it is now available
      if @decryptState == DS_WAITNONCE 
        break if @inputBuffer.size <  NONCE_SIZE_SMALL 
        setNonce(@inputBuffer.slice!(0...NONCE_SIZE_SMALL))  
        @decryptState = DS_WAITCHUNK
        next
      end
      
      # If we don't have a full chunk, exit
      # (the last chunk may be smaller; we'll test for this when finishing up)
      break if @inputBuffer.size < CHUNK_SIZE_DECR
      
    else 
      break if @inputBuffer.size < CHUNK_SIZE_ENCR
    end  
    
    # Process chunk and repeat
    processChunk()
  end
end

#finish(data = nil) ⇒ Object

Stop the encryption/decryption process.

Processes any bytes that may have been buffered (since encryption occurs in 16 byte blocks at a time).

Parameters:

  • data (defaults to: nil)

    optional final input string to process before finishing

Raises:



389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
# File 'lib/lenc/aes.rb', line 389

def finish(data = nil) 
  
  add(data) if data 
    
  raise IllegalStateException if @finished  
  
  @finished = true
  
  inpLen = @inputBuffer.size
  
  if @encrypting 
    # If input buffer is not empty, or we haven't written a first chunk (which contains the nonce),
    # encrypt a chunk
    if inpLen or (not @nonceWritten) 
      processChunk()
    end
  else
    
    # We must be at WAITCHUNK with an input buffer that is a multiple of _AES_BLOCK_SIZE bytes in length
    if @decryptState != DS_WAITCHUNK or 0 != (inpLen & (AES_BLOCK_SIZE-1)) 
      raise LEnc::DecryptionError, "decrypt state problem"
    end
    
    # We expect a chunk if there's more input, or if we've never processed a chunk.
    if inpLen != 0 or @chunkCount == 0
      processChunk()
    end
  end
end

#flushObject

Return any output bytes that have been generated since the last call to flush()

Returns:

  • string containing bytes



422
423
424
425
426
# File 'lib/lenc/aes.rb', line 422

def flush()
  ret = @outputBuffer 
  @outputBuffer = ''
  return ret
end

#strip_encryption_header(encr_str) ⇒ Object

Strip the header from an encrypted string



429
430
431
# File 'lib/lenc/aes.rb', line 429

def strip_encryption_header( encr_str)
  return encr_str[CHUNK_HEADER_SIZE..-1]
end