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