lib/rmega/session.rb in rmega-0.1.7 vs lib/rmega/session.rb in rmega-0.2.0

- old
+ new

@@ -1,82 +1,155 @@ -require 'rmega/storage' -require 'rmega/errors' -require 'rmega/crypto/crypto' -require 'rmega/utils' - module Rmega - def self.login(email, password) - Session.new(email, password).storage - end - class Session + include NotInspectable include Loggable + include Net + include Options + include Crypto + extend Crypto - attr_reader :email, :request_id, :sid, :master_key, :shared_keys, :rsa_privk + attr_reader :request_id, :sid, :shared_keys, :rsa_privk + attr_accessor :master_key - def initialize(email, password) - @email = email + def initialize @request_id = random_request_id @shared_keys = {} + end - login(password) + def storage + @storage ||= Storage.new(self) end - def options - Rmega.options + def decrypt_rsa_private_key(encrypted_privk) + privk = aes_ecb_decrypt(@master_key, Utils.base64urldecode(encrypted_privk)) + + # Decompose private key + decomposed_key = [] + + 4.times do + len = ((privk[0].ord * 256 + privk[1].ord + 7) >> 3) + 2 + privk_part = privk[0, len] + decomposed_key << Utils.string_to_bignum(privk[0..len-1][2..-1]) + privk = privk[len..-1] + end + + return decomposed_key end - delegate :api_url, :api_request_timeout, to: :options - delegate :max_retries, :retry_interval, to: :options + def hash_password(password) + self.class.hash_password(password) + end - def storage - @storage ||= Storage.new(self) + def self.hash_password(password) + pwd = password.dup.force_encoding('BINARY') + pkey = "\x93\xc4\x67\xe3\x7d\xb0\xc7\xa4\xd1\xbe\x3f\x81\x1\x52\xcb\x56".force_encoding('BINARY') + null_byte = "\x0".force_encoding('BINARY').freeze + blank = (null_byte*16).force_encoding('BINARY').freeze + keys = {} + + 65536.times do + (0..pwd.size-1).step(16) do |j| + + keys[j] ||= begin + key = blank.dup + 16.times { |i| key[i] = pwd[i+j] || null_byte if i+j < pwd.size } + key + end + + pkey = aes_ecb_encrypt(keys[j], pkey) + end + end + + return pkey end - def login(password) - uh = Crypto.stringhash Crypto.prepare_key_pw(password), email.downcase - resp = request(a: 'us', user: email, uh: uh) + def decrypt_session_id(csid) + csid = Utils.base64_mpi_to_bn(csid) + csid = rsa_decrypt(csid, @rsa_privk) + csid = csid.to_s(16) + csid = '0' + csid if csid.length % 2 > 0 + csid = Utils.hexstr_to_bstr(csid)[0,43] + csid = Utils.base64urlencode(csid) + return csid + end - # Decrypts the master key - encrypted_key = Crypto.prepare_key_pw(password) - @master_key = Crypto.decrypt_key(encrypted_key, Utils.base64_to_a32(resp['k'])) + def user_hash(aes_key, email) + s_bytes = email.bytes.to_a + hash = Array.new(16, 0) + s_bytes.size.times { |n| hash[n & 15] = hash[n & 15] ^ s_bytes[n] } + hash = hash.pack('c*') + 16384.times { hash = aes_ecb_encrypt(aes_key, hash) } + hash = hash[0..4-1] + hash[8..12-1] + return Utils.base64urlencode(hash) + end - # Generates the session id - @rsa_privk = Crypto.decrypt_rsa_privk(@master_key, resp['privk']) - @sid = Crypto.decrypt_sid(@rsa_privk, resp['csid']) + # If the user_hash is found on the server it returns: + # * The user master_key (128 bit for AES) encrypted with the password_hash + # * The RSA private key ecrypted with the master_key + # * A brand new session_id encrypted with the RSA private key + def login(email, password) + # Derive an hash from the user password + password_hash = hash_password(password) + u_hash = user_hash(password_hash, email.strip.downcase) + + resp = request(a: 'us', user: email.strip, uh: u_hash) + + @master_key = aes_cbc_decrypt(password_hash, Utils.base64urldecode(resp['k'])) + @rsa_privk = decrypt_rsa_private_key(resp['privk']) + @sid = decrypt_session_id(resp['csid']) + @shared_keys = {} + + return self end - def random_request_id - rand(1E7..1E9).to_i + def ephemeral_login(user_handle, password) + resp = request(a: 'us', user: user_handle) + + password_hash = hash_password(password) + + @master_key = aes_cbc_decrypt(password_hash, Utils.base64urldecode(resp['k'])) + @sid = resp['tsid'] + @rsa_privk = nil + @shared_keys = {} + + return self end - def request_url - "#{api_url}?id=#{@request_id}".tap do |url| - url << "&sid=#{@sid}" if @sid - end + def self.ephemeral + master_key = OpenSSL::Random.random_bytes(16) + password = OpenSSL::Random.random_bytes(16) + password_hash = hash_password(password) + challenge = OpenSSL::Random.random_bytes(16) + + session = new + + user_handle = session.request(a: 'up', k: Utils.base64urlencode(aes_ecb_encrypt(password_hash, master_key)), + ts: Utils.base64urlencode(challenge + aes_ecb_encrypt(master_key, challenge))) + + return session.ephemeral_login(user_handle, password) end - def request(content, retries = max_retries) - @request_id += 1 - logger.debug "POST #{request_url} #{content.inspect}" + def random_request_id + rand(1E7..1E9).to_i + end - response = HTTPClient.new.post(request_url, [content].to_json, timeout: api_request_timeout) - code, body = response.code.to_i, response.body + def request_url(params = {}) + params = params.merge(sid: @sid) if @sid + params = params.to_a.map { |a| a.join("=") }.join("&") + params = "&#{params}" unless params.empty? - logger.debug("#{code} #{body}") + return "#{options.api_url}?id=#{@request_id}#{params}" + end - if code == 500 && body.to_s.empty? - raise Errors::ServerError.new("Server too busy", temporary: true) - else - json = JSON.parse(body).first - raise Errors::ServerError.new(json) if json.to_s =~ /\A\-\d+\z/ - json + def request(body, query_params = {}) + survive do + @request_id += 1 + api_response = APIResponse.new(http_post(request_url(query_params), [body].to_json)) + if api_response.ok? + return(api_response.as_json) + else + raise(api_response.as_error) + end end - rescue SocketError, Errors::ServerError => error - raise(error) if retries < 0 - raise(error) if error.respond_to?(:temporary?) && !error.temporary? - retries -= 1 - sleep(retry_interval) - retry end end end