Initial implementation of StoredFile
authorLunar <lunar@anargeek.net>
Sun, 18 Jul 2010 16:32:40 +0000 (18:32 +0200)
committerLunar <lunar@anargeek.net>
Sun, 18 Jul 2010 16:32:40 +0000 (18:32 +0200)
coquelicot.rb

index 05cc6d4..e99a953 100644 (file)
 require 'sinatra'
 require 'haml'
 require 'digest/sha1'
+require 'base64'
+require 'openssl'
 require 'singleton'
 
 enable :inline_templates
 
 set :upload_password, '0e5f7d398e6f9cd1f6bac5cc823e363aec636495'
 
+class StoredFile
+  def self.open(path, pass)
+    StoredFile.new(path, pass)
+  end
+
+  def each
+    # output content
+    yield @initial_content
+    @initial_content = nil
+    while "" != (buf = @file.read(BUFFER_LEN))
+      yield @cipher.update(buf)
+    end
+    yield @cipher.final
+    @cipher.reset
+    @cipher = nil
+  end
+
+  def self.create(path, pass, meta, content)
+    File.new(path, 'w') do |file|
+      salt = gen_salt
+      clear_meta = { "Coquelicot" => COQUELICOT_VERSION,
+                     "Salt" => Base64.encode64(salt).strip }
+      YAML.dump(clear_meta, file)
+      file.write YAML_START
+
+      cipher = get_cipher(pass, salt, :encrypt)
+      file << cipher.update(YAML.dump(meta) + YAML_START)
+      while '' != (buf = content.read(BUFFER_LEN)) do
+        file << cipher.update(buf)
+      end
+      file << cipher.final
+    end
+  end
+
+private
+
+  YAML_START = "---\n"
+  CIPHER = 'AES-256-CBC'
+  BUFFER_LEN = 4096
+  COQUELICOT_VERSION = "1.0"
+
+  def self.get_cipher(pass, salt, method)
+    hmac = PKCS5.pbkdf2_hmac_sha1(pass, salt, 2000, 48)
+    cipher = OpenSSL::Cipher.new CIPHER
+    cipher.call(method)
+    cipher.key = hmac[0..31]
+    cipher.iv = hmac[32..-1]
+    cipher
+  end
+
+  def initialize(path, pass)
+    @file = File.open(path)
+    if YAML_START != (buf = @file.read(YAML_START.length)) then
+      raise "unknown file, read #{buf.inspect}"
+    end
+    parse_clear_meta
+    init_decrypt_cipher pass
+    parse_meta
+  end
+
+  def parse_clear_meta
+    while YAML_START != (line = @file.readline) do
+      meta += line
+    end
+    @meta = YAML.load(meta)
+    if @meta["Coquelicot"].nil? or @meta["Coquelicot"] != COQUELICOT_VERSION then
+      raise "unknown file"
+    end
+  end
+
+  def init_decrypt_cipher(pass)
+    salt = Base64.decode(@meta["Salt"])
+    @cipher = get_cipher(pass, salt, :decrypt)
+  end
+
+  def parse_meta
+    buf = @file.read(BUFFER_LEN)
+    yaml = ''
+    yaml << @cipher.update(buf)
+    unless yaml.start_with? YAML_START
+      raise "bad key"
+    end
+    while "" != (buf = @file.read(BUFFER_LEN))
+      block = @cipher.update(buf).split(/^---$/, 2)
+      yaml << block[0]
+      loop unless block.length == 2
+      @meta.merge(YAML.load(yaml))
+      @initial_content = block[1]
+    end
+  end
+
+  def close
+    @cipher.reset unless @cipher.nil?
+    @file.close
+  end
+end
+
 def password_match?(password)
   return TRUE if settings.upload_password.nil?
   (not password.nil?) && Digest::SHA1.hexdigest(password) == settings.upload_password