lastpass 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ Version 1.1.0
2
+ -------------
3
+
4
+ - Shared folder support
5
+
6
+
1
7
  Version 1.0.1
2
8
  -------------
3
9
 
data/README.md CHANGED
@@ -8,6 +8,8 @@ LastPass Ruby API
8
8
 
9
9
  **This is unofficial LastPass API.**
10
10
 
11
+ There's also [C#/.NET port](https://github.com/detunized/lastpass-sharp) available.
12
+
11
13
  This library implements fetching and parsing of LastPass data. The library is
12
14
  still in the proof of concept stage and doesn't support all LastPass features
13
15
  yet. Only account information (logins, passwords, urls, etc.) is available so
@@ -30,10 +32,17 @@ vault.accounts.each do |i|
30
32
  end
31
33
  ```
32
34
 
35
+
36
+ A multifactor password (YubiKey, Google Authenticator) can optionally be appended to
37
+ the login credentials:
38
+
39
+ ```ruby
40
+ vault = LastPass::Vault.open_remote "username", "password", "multifactor_password"
41
+ ```
42
+
33
43
  The blob received from LastPass could be safely stored locally (it's well
34
44
  encrypted) and reused later on.
35
45
 
36
-
37
46
  LostPass iOS App
38
47
  ----------------
39
48
 
data/TODO ADDED
@@ -0,0 +1,5 @@
1
+ - Make tests more consistent. Currently there's a lot of different binary blobs stored in the tests.
2
+ It would be good to generate the test data from some meaningful pieces. Like construct chunks from
3
+ items, construct items themselves. Build the whole blob out of chunks. This way the tests would be
4
+ more readable and easier to update in case of format changes. This also fixes the problem of
5
+ exposing real world usernames, passwords and encryption keys.
data/example/example.rb CHANGED
@@ -12,8 +12,25 @@ credentials = YAML.load_file File.join File.dirname(__FILE__), "credentials.yaml
12
12
  username = credentials["username"]
13
13
  password = credentials["password"]
14
14
 
15
- vault = LastPass::Vault.open_remote username, password
15
+ begin
16
+ # First try without a multifactor password
17
+ vault = LastPass::Vault.open_remote username, password
18
+ rescue LastPass::LastPassIncorrectGoogleAuthenticatorCodeError => e
19
+ # Get the code
20
+ puts "Enter Google Authenticator code:"
21
+ multifactor_password = gets.chomp
22
+
23
+ # And now retry with the code
24
+ vault = LastPass::Vault.open_remote username, password, multifactor_password
25
+ rescue LastPass::LastPassIncorrectYubikeyPasswordError => e
26
+ # Get the password
27
+ puts "Enter Yubikey password:"
28
+ multifactor_password = gets.chomp
29
+
30
+ # And now retry with the Yubikey password
31
+ vault = LastPass::Vault.open_remote username, password, multifactor_password
32
+ end
16
33
 
17
34
  vault.accounts.each_with_index do |i, index|
18
- puts "#{index + 1}: #{i.id} #{i.name} #{i.username} #{i.password} #{i.url} #{i.group}}"
35
+ puts "#{index + 1}: #{i.id} #{i.name} #{i.username} #{i.password} #{i.url} #{i.group}"
19
36
  end
data/lastpass.gemspec CHANGED
@@ -16,10 +16,10 @@ Gem::Specification.new do |s|
16
16
 
17
17
  s.required_ruby_version = ">= 1.9.3"
18
18
 
19
- s.add_dependency "httparty", "~> 0.12.0"
19
+ s.add_dependency "httparty", "~> 0.13.0"
20
20
  s.add_dependency "pbkdf2-ruby", "~> 0.2.0"
21
21
 
22
- s.add_development_dependency "rake", "~> 10.0.0"
22
+ s.add_development_dependency "rake", "~> 10.1.0"
23
23
  s.add_development_dependency "rspec", "~> 2.14.0"
24
24
  s.add_development_dependency "coveralls", "~> 0.7.0"
25
25
 
@@ -13,20 +13,26 @@ module LastPass
13
13
  class NetworkError < Error; end
14
14
 
15
15
  # Server responded with something we don't understand
16
- class InvalidResponse < Error; end
16
+ class InvalidResponseError < Error; end
17
17
 
18
18
  # Server responded with XML we don't understand
19
- class UnknownResponseSchema < Error; end
19
+ class UnknownResponseSchemaError < Error; end
20
20
 
21
21
  #
22
22
  # LastPass returned errors
23
23
  #
24
24
 
25
25
  # LastPass error: unknown username
26
- class LastPassUnknownUsername < Error; end
26
+ class LastPassUnknownUsernameError < Error; end
27
27
 
28
28
  # LastPass error: invalid password
29
- class LastPassInvalidPassword < Error; end
29
+ class LastPassInvalidPasswordError < Error; end
30
+
31
+ # LastPass error: missing or incorrect Google Authenticator code
32
+ class LastPassIncorrectGoogleAuthenticatorCodeError < Error; end
33
+
34
+ # LastPass error: missing or incorrect Yubikey password
35
+ class LastPassIncorrectYubikeyPasswordError < Error; end
30
36
 
31
37
  # LastPass error we don't know about
32
38
  class LastPassUnknownError < Error; end
@@ -3,13 +3,13 @@
3
3
 
4
4
  module LastPass
5
5
  class Fetcher
6
- def self.login username, password
6
+ def self.login username, password, multifactor_password = nil
7
7
  key_iteration_count = request_iteration_count username
8
- request_login username, password, key_iteration_count
8
+ request_login username, password, key_iteration_count, multifactor_password
9
9
  end
10
10
 
11
11
  def self.fetch session, web_client = HTTParty
12
- response = web_client.get "https://lastpass.com/getaccts.php?mobile=1&b64=1&hash=0.0",
12
+ response = web_client.get "https://lastpass.com/getaccts.php?mobile=1&b64=1&hash=0.0&hasplugin=3.0.23&requestsrc=android",
13
13
  format: :plain,
14
14
  cookies: {"PHPSESSID" => URI.encode(session.id)}
15
15
 
@@ -27,30 +27,39 @@ module LastPass
27
27
  begin
28
28
  count = Integer response.parsed_response
29
29
  rescue ArgumentError
30
- raise InvalidResponse, "Key iteration count is invalid"
30
+ raise InvalidResponseError, "Key iteration count is invalid"
31
31
  end
32
32
 
33
- raise InvalidResponse, "Key iteration count is not positive" unless count > 0
33
+ raise InvalidResponseError, "Key iteration count is not positive" unless count > 0
34
34
 
35
35
  count
36
36
  end
37
37
 
38
- def self.request_login username, password, key_iteration_count, web_client = HTTParty
38
+ def self.request_login username,
39
+ password,
40
+ key_iteration_count,
41
+ multifactor_password = nil,
42
+ web_client = HTTParty
43
+
44
+ body = {
45
+ method: "mobile",
46
+ web: 1,
47
+ xml: 1,
48
+ username: username,
49
+ hash: make_hash(username, password, key_iteration_count),
50
+ iterations: key_iteration_count
51
+ }
52
+
53
+ body[:otp] = multifactor_password if multifactor_password
54
+
39
55
  response = web_client.post "https://lastpass.com/login.php",
40
56
  format: :xml,
41
- body: {
42
- method: "mobile",
43
- web: 1,
44
- xml: 1,
45
- username: username,
46
- hash: make_hash(username, password, key_iteration_count),
47
- iterations: key_iteration_count
48
- }
57
+ body: body
49
58
 
50
59
  raise NetworkError unless response.response.is_a? Net::HTTPOK
51
60
 
52
61
  parsed_response = response.parsed_response
53
- raise InvalidResponse unless parsed_response.is_a? Hash
62
+ raise InvalidResponseError unless parsed_response.is_a? Hash
54
63
 
55
64
  create_session parsed_response, key_iteration_count or
56
65
  raise login_error parsed_response
@@ -70,11 +79,14 @@ module LastPass
70
79
 
71
80
  def self.login_error parsed_response
72
81
  error = (parsed_response["response"] || {})["error"]
73
- return UnknownResponseSchema unless error.is_a? Hash
82
+ return UnknownResponseSchemaError unless error.is_a? Hash
74
83
 
75
84
  exceptions = {
76
- "unknownemail" => LastPassUnknownUsername,
77
- "unknownpassword" => LastPassInvalidPassword,
85
+ "unknownemail" => LastPassUnknownUsernameError,
86
+ "unknownpassword" => LastPassInvalidPasswordError,
87
+ "googleauthrequired" => LastPassIncorrectGoogleAuthenticatorCodeError,
88
+ "googleauthfailed" => LastPassIncorrectGoogleAuthenticatorCodeError,
89
+ "yubikeyrestricted" => LastPassIncorrectYubikeyPasswordError,
78
90
  }
79
91
 
80
92
  cause = error["cause"]
@@ -83,7 +95,7 @@ module LastPass
83
95
  if cause
84
96
  (exceptions[cause] || LastPassUnknownError).new message || cause
85
97
  else
86
- InvalidResponse.new message
98
+ InvalidResponseError.new message
87
99
  end
88
100
  end
89
101
 
@@ -3,14 +3,16 @@
3
3
 
4
4
  module LastPass
5
5
  class Parser
6
+ # OpenSSL constant
7
+ RSA_PKCS1_OAEP_PADDING = 4
8
+
6
9
  # Splits the blob into chucks grouped by kind.
7
10
  def self.extract_chunks blob
8
- chunks = Hash.new { |hash, key| hash[key] = [] }
11
+ chunks = []
9
12
 
10
13
  StringIO.open blob.bytes do |stream|
11
14
  while !stream.eof?
12
- chunk = read_chunk stream
13
- chunks[chunk.id] << chunk
15
+ chunks.push read_chunk stream
14
16
  end
15
17
  end
16
18
 
@@ -18,8 +20,7 @@ module LastPass
18
20
  end
19
21
 
20
22
  # Parses an account chunk, decrypts and creates an Account object.
21
- # TODO: See if this should be part of Account class.
22
- def self.parse_account chunk, encryption_key
23
+ def self.parse_ACCT chunk, encryption_key
23
24
  StringIO.open chunk.payload do |io|
24
25
  id = read_item io
25
26
  name = decode_aes256_auto read_item(io), encryption_key
@@ -33,6 +34,56 @@ module LastPass
33
34
  end
34
35
  end
35
36
 
37
+ # Parse PRIK chunk which contains private RSA key
38
+ def self.parse_PRIK chunk, encryption_key
39
+ decrypted = decode_aes256 "cbc",
40
+ encryption_key[0, 16],
41
+ decode_hex(chunk.payload),
42
+ encryption_key
43
+
44
+ /^LastPassPrivateKey<(?<hex_key>.*)>LastPassPrivateKey$/ =~ decrypted
45
+ asn1_encoded_all = OpenSSL::ASN1.decode decode_hex hex_key
46
+ asn1_encoded_key = OpenSSL::ASN1.decode asn1_encoded_all.value[2].value
47
+
48
+ rsa_key = OpenSSL::PKey::RSA.new
49
+ rsa_key.n = asn1_encoded_key.value[1].value
50
+ rsa_key.e = asn1_encoded_key.value[2].value
51
+ rsa_key.d = asn1_encoded_key.value[3].value
52
+ rsa_key.p = asn1_encoded_key.value[4].value
53
+ rsa_key.q = asn1_encoded_key.value[5].value
54
+ rsa_key.dmp1 = asn1_encoded_key.value[6].value
55
+ rsa_key.dmq1 = asn1_encoded_key.value[7].value
56
+ rsa_key.iqmp = asn1_encoded_key.value[8].value
57
+
58
+ rsa_key
59
+ end
60
+
61
+ # TODO: Fake some data and make a test
62
+ def self.parse_SHAR chunk, encryption_key, rsa_key
63
+ StringIO.open chunk.payload do |io|
64
+ id = read_item io
65
+ encrypted_key = decode_hex read_item io
66
+ encrypted_name = read_item io
67
+ 2.times { skip_item io }
68
+ key = read_item io
69
+
70
+ # Shared folder encryption key might come already in pre-decrypted form,
71
+ # where it's only AES encrypted with the regular encryption key.
72
+ # When the key is blank, then there's a RSA encrypted key, which has to
73
+ # be decrypted first before use.
74
+ key = if key.empty?
75
+ decode_hex rsa_key.private_decrypt(encrypted_key, RSA_PKCS1_OAEP_PADDING)
76
+ else
77
+ decode_hex decode_aes256_auto(key, encryption_key)
78
+ end
79
+
80
+ name = decode_aes256_auto encrypted_name, key
81
+
82
+ # TODO: Return an object, not a hash
83
+ {id: id, name: name, encryption_key: key}
84
+ end
85
+ end
86
+
36
87
  # Reads one chunk from a stream and creates a Chunk object with the data read.
37
88
  def self.read_chunk stream
38
89
  # LastPass blob chunk is made up of 4-byte ID,
@@ -6,8 +6,8 @@ module LastPass
6
6
  attr_reader :accounts
7
7
 
8
8
  # Fetches a blob from the server and creates a vault
9
- def self.open_remote username, password
10
- open Vault.fetch_blob(username, password), username, password
9
+ def self.open_remote username, password, multifactor_password = nil
10
+ open Vault.fetch_blob(username, password, multifactor_password), username, password
11
11
  end
12
12
 
13
13
  # Creates a vault from a locally stored blob
@@ -21,14 +21,29 @@ module LastPass
21
21
  end
22
22
 
23
23
  # Just fetches the blob, could be used to store it locally
24
- def self.fetch_blob username, password
25
- Fetcher.fetch Fetcher.login username, password
24
+ def self.fetch_blob username, password, multifactor_password = nil
25
+ Fetcher.fetch Fetcher.login username, password, multifactor_password
26
26
  end
27
27
 
28
28
  # This more of an internal method, use one of the static constructors instead
29
29
  def initialize blob, encryption_key
30
- chunks = Parser.extract_chunks blob
31
- @accounts = (chunks["ACCT"] || []).map { |i| Parser.parse_account i, encryption_key }
30
+ @accounts = []
31
+
32
+ key = encryption_key
33
+ rsa_private_key = nil
34
+
35
+ Parser.extract_chunks(blob).each do |i|
36
+ case i.id
37
+ when "ACCT"
38
+ # TODO: Put shared folder name as group in the account
39
+ @accounts.push Parser.parse_ACCT i, key
40
+ when "PRIK"
41
+ rsa_private_key = Parser.parse_PRIK i, encryption_key
42
+ when "SHAR"
43
+ # After SHAR chunk all the folliwing accounts are enrypted with a new key
44
+ key = Parser.parse_SHAR(i, encryption_key, rsa_private_key)[:encryption_key]
45
+ end
46
+ end
32
47
  end
33
48
  end
34
49
  end
@@ -2,5 +2,5 @@
2
2
  # Licensed under the terms of the MIT license. See LICENCE for details.
3
3
 
4
4
  module LastPass
5
- VERSION = "1.0.1"
5
+ VERSION = "1.1.0"
6
6
  end
data/spec/fetcher_spec.rb CHANGED
@@ -7,12 +7,28 @@ describe LastPass::Fetcher do
7
7
  let(:username) { "username" }
8
8
  let(:password) { "password" }
9
9
  let(:key_iteration_count) { 5000 }
10
+
11
+ let(:hash) { "7880a04588cfab954aa1a2da98fd9c0d2c6eba4c53e36a94510e6dbf30759256" }
10
12
  let(:session_id) { "53ru,Hb713QnEVM5zWZ16jMvxS0" }
11
13
  let(:session) { LastPass::Session.new session_id, key_iteration_count }
14
+
12
15
  let(:blob_response) { "TFBBVgAAAAMxMjJQUkVNAAAACjE0MTQ5" }
13
16
  let(:blob_bytes) { blob_response.decode64 }
14
17
  let(:blob) { LastPass::Blob.new blob_bytes, key_iteration_count }
15
18
 
19
+ let(:login_post_data) { {method: "mobile",
20
+ web: 1,
21
+ xml: 1,
22
+ username: username,
23
+ hash: hash,
24
+ iterations: key_iteration_count} }
25
+
26
+ let(:google_authenticator_code) { "123456" }
27
+ let(:yubikey_password) { "emdbwzemyisymdnevznyqhqnklaqheaxszzvtnxjrmkb" }
28
+
29
+ let(:login_post_data_with_google_authenticator_code) { login_post_data.merge({otp: google_authenticator_code})}
30
+ let(:login_post_data_with_yubikey_password) { login_post_data.merge({otp: yubikey_password}) }
31
+
16
32
  describe ".request_iteration_count" do
17
33
  it "makes a POST request" do
18
34
  expect(web_client = double("web_client")).to receive(:post)
@@ -40,31 +56,48 @@ describe LastPass::Fetcher do
40
56
  expect {
41
57
  LastPass::Fetcher.request_iteration_count username,
42
58
  double("web_client", post: http_ok("not a number"))
43
- }.to raise_error LastPass::InvalidResponse, "Key iteration count is invalid"
59
+ }.to raise_error LastPass::InvalidResponseError, "Key iteration count is invalid"
44
60
  end
45
61
 
46
62
  it "raises an exception on zero key iteration count" do
47
63
  expect {
48
64
  LastPass::Fetcher.request_iteration_count username,
49
65
  double("web_client", post: http_ok("0"))
50
- }.to raise_error LastPass::InvalidResponse, "Key iteration count is not positive"
66
+ }.to raise_error LastPass::InvalidResponseError, "Key iteration count is not positive"
51
67
  end
52
68
 
53
69
  it "raises an exception on negative key iteration count" do
54
70
  expect {
55
71
  LastPass::Fetcher.request_iteration_count username,
56
72
  double("web_client", post: http_ok("-1"))
57
- }.to raise_error LastPass::InvalidResponse, "Key iteration count is not positive"
73
+ }.to raise_error LastPass::InvalidResponseError, "Key iteration count is not positive"
58
74
  end
59
75
  end
60
76
 
61
77
  describe ".request_login" do
62
- it "makes a POST request" do
63
- expect(web_client = double("web_client")).to receive(:post)
64
- .with("https://lastpass.com/login.php", format: :xml, body: anything)
78
+ def verify_post_request multifactor_password, post_data
79
+ web_client = double("web_client")
80
+ expect(web_client).to receive(:post)
81
+ .with("https://lastpass.com/login.php", format: :xml, body: post_data)
65
82
  .and_return(http_ok("ok" => {"sessionid" => session_id}))
66
83
 
67
- LastPass::Fetcher.request_login username, password, key_iteration_count, web_client
84
+ LastPass::Fetcher.request_login username,
85
+ password,
86
+ key_iteration_count,
87
+ multifactor_password,
88
+ web_client
89
+ end
90
+
91
+ it "makes a POST request" do
92
+ verify_post_request nil, login_post_data
93
+ end
94
+
95
+ it "makes a POST request with Google Authenticator code" do
96
+ verify_post_request google_authenticator_code, login_post_data_with_google_authenticator_code
97
+ end
98
+
99
+ it "makes a POST request with Yubikey password" do
100
+ verify_post_request yubikey_password, login_post_data_with_yubikey_password
68
101
  end
69
102
 
70
103
  it "returns a session" do
@@ -76,32 +109,52 @@ describe LastPass::Fetcher do
76
109
  end
77
110
 
78
111
  it "raises an exception when response is not a hash" do
79
- expect { request_login_with_ok "not a hash" }.to raise_error LastPass::InvalidResponse
112
+ expect { request_login_with_ok "not a hash" }.to raise_error LastPass::InvalidResponseError
80
113
  end
81
114
 
82
115
  it "raises an exception on unknown response schema" do
83
- expect { request_login_with_xml "<unknown />" }.to raise_error LastPass::UnknownResponseSchema
116
+ expect { request_login_with_xml "<unknown />" }.to raise_error LastPass::UnknownResponseSchemaError
84
117
  end
85
118
 
86
119
  it "raises an exception on unknown response schema" do
87
- expect { request_login_with_xml "<response />" }.to raise_error LastPass::UnknownResponseSchema
120
+ expect { request_login_with_xml "<response />" }.to raise_error LastPass::UnknownResponseSchemaError
88
121
  end
89
122
 
90
123
  it "raises an exception on unknown response schema" do
91
124
  expect { request_login_with_xml "<response><error /></response>" }
92
- .to raise_error LastPass::UnknownResponseSchema
125
+ .to raise_error LastPass::UnknownResponseSchemaError
93
126
  end
94
127
 
95
128
  it "raises an exception on unknown username" do
96
129
  message = "Unknown email address."
97
130
  expect { request_login_with_lastpass_error "unknownemail", message }
98
- .to raise_error LastPass::LastPassUnknownUsername, message
131
+ .to raise_error LastPass::LastPassUnknownUsernameError, message
99
132
  end
100
133
 
101
134
  it "raises an exception on invalid password" do
102
135
  message = "Invalid password!"
103
136
  expect { request_login_with_lastpass_error "unknownpassword", message }
104
- .to raise_error LastPass::LastPassInvalidPassword, message
137
+ .to raise_error LastPass::LastPassInvalidPasswordError, message
138
+ end
139
+
140
+ it "raises an exception on missing Google Authenticator code" do
141
+ message = "Google Authenticator authentication required! " +
142
+ "Upgrade your browser extension so you can enter it."
143
+ expect { request_login_with_lastpass_error "googleauthrequired", message }
144
+ .to raise_error LastPass::LastPassIncorrectGoogleAuthenticatorCodeError, message
145
+ end
146
+
147
+ it "raises an exception on incorrect Google Authenticator code" do
148
+ message = "Google Authenticator authentication failed!"
149
+ expect { request_login_with_lastpass_error "googleauthfailed", message }
150
+ .to raise_error LastPass::LastPassIncorrectGoogleAuthenticatorCodeError, message
151
+ end
152
+
153
+ it "raises an exception on missing/incorrect Yubikey password" do
154
+ message = "Your account settings have restricted you from logging in " +
155
+ "from mobile devices that do not support YubiKey authentication."
156
+ expect { request_login_with_lastpass_error "yubikeyrestricted", message }
157
+ .to raise_error LastPass::LastPassIncorrectYubikeyPasswordError, message
105
158
  end
106
159
 
107
160
  it "raises an exception on unknown LastPass error with a message" do
@@ -120,7 +173,7 @@ describe LastPass::Fetcher do
120
173
  describe ".fetch" do
121
174
  it "makes a GET request" do
122
175
  expect(web_client = double("web_client")).to receive(:get)
123
- .with("https://lastpass.com/getaccts.php?mobile=1&b64=1&hash=0.0",
176
+ .with("https://lastpass.com/getaccts.php?mobile=1&b64=1&hash=0.0&hasplugin=3.0.23&requestsrc=android",
124
177
  format: :plain,
125
178
  cookies: {"PHPSESSID" => session_id})
126
179
  .and_return(http_ok(blob_response))
@@ -223,6 +276,7 @@ describe LastPass::Fetcher do
223
276
  LastPass::Fetcher.request_login username,
224
277
  password,
225
278
  key_iteration_count,
279
+ nil,
226
280
  double("web_client", post: response)
227
281
  end
228
282
  end
data/spec/parser_spec.rb CHANGED
@@ -9,38 +9,104 @@ describe LastPass::Parser do
9
9
  let(:blob) { LastPass::Blob.new TEST_BLOB, key_iteration_count }
10
10
  let(:padding) { "BEEFFACE"}
11
11
  let(:encryption_key) { "OfOUvVnQzB4v49sNh4+PdwIFb9Fr5+jVfWRTf+E2Ghg=".decode64 }
12
+ let(:encoded_rsa_key) { "98F3F5518AE7C03EBBF195A616361619033509FB1FFA0408E883B7C5E80381F8" +
13
+ "C8A343925DDA78FB06A14324BEC77EAF63290D381F54763A2793FE25C3247FC0" +
14
+ "29022687F453426DE96A9FB34CEB55C02764FB41E5E1619226FE47FA7EA40B41" +
15
+ "0973132F7AB2DE2D7F08C181C7D56BBF92CD4D44BC7DEE4253DEC36C77D28E30" +
16
+ "6F41B8BB26B0EDB97BADCEE912D3671C22339036FC064F5AF60D3545D47B8263" +
17
+ "6BBA1896ECDCF5EBE99A1061EFB8FBBD6C3500EA06A28BB8863F413702D9C05B" +
18
+ "9A54120F1BEFA0D98A48E82622A36DBD79772B5E4AD957045DC2B97311983592" +
19
+ "A357037DDA172C284B4FEC7DF8962A11B42079D6F943C8F9C0FEDFEA0C43A362" +
20
+ "B550E217715FD82D9F3BB168A006B0880B1F3660076158FE8CF6B706CF2FEAA1" +
21
+ "A731D1F68B1BC20E7ADE15097D2CD84606B4B0756DFE25DAF110D62841F44265" +
22
+ "73A676B904972B31AD7B02093C536341E1DA943F1AFF88DF2005BD04C6897FB6" +
23
+ "F9E307DA1C2BD219AB39F911FF90C6B1EA658C72C67C1EADC36CD5202654B4E1" +
24
+ "99A88F13DCE1148CC04F81485896627BB1DB5C73969520CC66652492383930E3" +
25
+ "3AFD57BE171F4BA25016EC9C3662F5B054101E381565433E46CB9FD517B59AE8" +
26
+ "A5CE7D11005282E551E9DCAA1996763E41B49677F906F122AAB76E852F35B31F" +
27
+ "397B70949D5F6C8DAA244AF16E9D48E0801E5C6D3FCEAFD2C3E157968B3E796C" +
28
+ "87E1F3FFF86B62FE5263D1A597E3906BF697C019F1F543D7BB1E11B08837B47F" +
29
+ "4528E4B47EB77508CFC0581B2A005383D0A238EA5BDE2E2602E0D2408B139735" +
30
+ "F4BAF8D6CF260BBC81833A85F14C5746AC6081B878486F5A4BD23B821F3F5F6B" +
31
+ "DAC8A9B57E25E24EDB8D701F01AE142D63A8A7D0F1CC8FAFF5F0320551CEB29B" +
32
+ "DB6907C57E38602927AD7240003FEB238AC5437FE4BAD11BB5038CA74D539523" +
33
+ "A167B8EBB1210608EB7DA53B4155D05B87D21848E58905EFA550EA5A51E0A68D" +
34
+ "5FF0F9E0CC0D5105DD98BE9E2C41362794A71A573CCA87B57147115B86FC8A6B" +
35
+ "B1778CED1920787271C75D69C5D63CD798915BF8F9877808F841F9269B2EA809" +
36
+ "0E11F6C89FDB537F341142CA29BAC761E1CF9D58FFB0C44A26E5EF7FA14142C8" +
37
+ "A84BC9304A221D5F961DB41B5925B06823A12A6F8950E47325021A747A02A28F" +
38
+ "DAE65997EBDF5D2BDBCA7C8D689AE186A9FE85A170B76EE92595C9E33639C993" +
39
+ "07C377FA4DA975E191810E993CDC0A33EE494B0EE8A1B6A9408285012967C17A" +
40
+ "8CB5EE8E7973CF9186A98000FE00F1CC76420089C6BDCE9E39D403C320DF1135" +
41
+ "1597FF8B231689389CCE12844289FEFE468BFCAEE9A2CFB1A8DD066AEC974DA9" +
42
+ "C8530C9A17593E25DC89934E056B178329C4BBF7113657677AB25EE66A1E1D92" +
43
+ "F62154B2451B37727F05B3AC0F2501F7A95845C9BE210D411028C27A9AD4B0E8" +
44
+ "31A6C46D26883A8AA2D1E2BD3E8E122A6FC21CECB7AE2B91C6FCFA793C5CAFF6" +
45
+ "53C6670D914A29EAD81CD5C29FFB048C81CC80EDD693B4D8091B2D5DE88EA042" +
46
+ "11AC551F406B713278BD14667E437C610953D6186C2986BA60361C2013395E8E" +
47
+ "A9D14CD00EC5C61147BE03D8965B5376DF32E2C3740128398E0D47900C888FD0" +
48
+ "D1F7D583808AFBC0712806E11462B37815C20692FB38E61CC0B1AAF66A854982" +
49
+ "6A1F5FFFF2436B0B9F9EDFF4F5B59B362AA1D25A4E3C398EB18445483F8419BD" +
50
+ "1511A5177E9C4B7034375A2D91B95153535E6CD5F023F4EED0E15B5415A3B7A7" +
51
+ "7E390AA698DF00F4FD897B0454C00959AF0CB54B272DE63968815B971C44B273" +
52
+ "6AC737FAE6A19F544907833F13C6F424D30E3B85054A4402EC94079C1473C20B" +
53
+ "E4C1B33525486BB098EF960082DB4DF5FE9CAF71681B03CB2D4BE7382FF0C03F" +
54
+ "18144DE554256591773DC3F381116955233FDA7223D71C402E558783F221E25A" +
55
+ "94FECD350654A9CD8EE8C39E4B1CFBA0D5FD46891527F2D0FC9EA61584A76D59" +
56
+ "99719811B2BAFC99769E6911733ED389A731C327CB5D7BB6D79CE030D3285586" +
57
+ "C6681FC8C110EFE30CEE883FFEF5FB511B4421863E2A15F8CDCFA7B84B931121" +
58
+ "5B23093DE3B5E7F4CFCCE60BE7857B7442B8FCC3E43C46C4BFA3E9ABD2F479F6" +
59
+ "BD8D3F3D36C0FAC1F4D72FBE96C644AB56F73CAF956D5544B2EB9C589ED30FF3" +
60
+ "0BB03D09DB455764EF4A33C24F93170A98A21455826390B13A8F338A820EC08D" +
61
+ "6E9F562282C2F815BB57CE511AB6B0DE75EFA63F28C6D0B25298CDAAC76742D5" +
62
+ "353B26B77C1533B4DFE2D95F3E89315C0D806A90FCDFDC31CE04A9E29937680D" +
63
+ "32D8B503352388109C1F5F41E8496302E13A61917F70A9AA3C5ECDBD88163E3C" +
64
+ "F0580C5EB1382BB66194AC0983BAA16B4D220756F4B7E3DDFFC5BF343FA7E31D" +
65
+ "14FED4409AD0FE9BBE01AF79DA4852253CBF166FDCA90E894B5267A502F73347" +
66
+ "06F8C767EC861324CC7734352D76DB007E25105E7994CF91D79532221316F4DE" +
67
+ "56BAE4351D3E3C6549FBFEF13BBE2636071794AD9EC3787B4A71E5438B86C358" +
68
+ "65ECF2EA5980318F82D8B113C0EC8FEE41C243E0A1A09F373A0CF546FA18E1EC" +
69
+ "7DB4842A6B8B03D115654222B87DA6034EFDE2224DBD23AB104BF3723856C03D" +
70
+ "B639BA073F2CC8E4AB05BAADDB5DEACC1874F4D6F86B95710019114DACBFE48F" +
71
+ "EF2AE2DF27356B5C17948B26A41FD1A8F07E8068E176F995910C373886DB47D2" +
72
+ "6C2FE5CD97AAF1829EBC1EEBA4D88343A322E810385138F51F0E5149183699C4" +
73
+ "05E49ED13C2889A22742893A52567B0F7D4A3BC9F4DC6D29F713AA7FB4EF6B13" +
74
+ "5F92F598404A80E7D6515CE234AFA68A4B562AF203162C60D578F0D00E302958" +
75
+ "174E1A712FD449D257C6AA5F56E4DBD0363573931463BC910858AF1EC40C1F4A" +
76
+ "7BE27DE8E170D4AACF6C34B0CDE15190FD81FA5676136A4D73E2AA4BBFBB8E7C" +
77
+ "1178EF47362188D9288E822B10BBF2C8BE075A5BD1D3E1F08108BA8C4E6FB173" +
78
+ "DCECB5771E9D8AE4CD776EA3409DF30FA2252D3C3769AF12177F4A1929DC8E74" +
79
+ "D5AEAC94CF94EEBA0E9AC012C57B40A8BB57530C25846B841005767B9AABE436" +
80
+ "D4590977FDDA519B9B284CF8B8922A0E8B659ECE3745A95800EE1B3DDD33E0FF" +
81
+ "230C0528BC7A4CB80604411E59E08775A42C634E93BA9C77D015659AC912F436" +
82
+ "94F774E94050E4B3BF84290368D5AFD7F043BDCA3BD0CC8C0E267069B6F1386A" +
83
+ "E1D9C8B5512AAAA292FDA9CA07E27BAF983E1E25A11732797425F2BB396B302E" +
84
+ "0782BA183D4BC1F682365774520EAC8A321C7A0BD08027021EA0063D471E0AD1" +
85
+ "E1469AD803C311D3FBF50B5538265D4262B6716D90E89A8C906D08533D650000" +
86
+ "6BF1B8ABAAFE1CA3AFDD1A19ACABE5B86A804D36AE27163CAF390FD266D5FFEF" +
87
+ "FC7CE6FEF9458E4AF0C4108E32EFD11C19751B1D9883E803F7C2E1A5786F3385" +
88
+ "1A7CA3772ECD7CB0E9782A7D30E0A9FD09EED361B774A277C618C995FD7F7634" +
89
+ "E7DB3834690B58DDFF6B721157D0EC02" }
90
+ let(:rsa_key_encryption_key) { "v4uHomAR0tAXC3fA5Nfq7DjyJxuvYErMSCcZIWZKjpM=".decode64 }
12
91
 
13
92
  describe ".extract_chunks" do
14
93
  context "returned chunks" do
15
94
  let(:chunks) { LastPass::Parser.extract_chunks blob }
16
95
 
17
- it { expect(chunks).to be_instance_of Hash }
96
+ it { expect(chunks).to be_instance_of Array }
18
97
 
19
- it "all keys are strings" do
20
- expect(chunks.keys).to match_array TEST_CHUNK_IDS
21
- end
22
-
23
- it "all values are arrays" do
24
- expect(chunks.values.map(&:class).uniq).to eq [Array]
25
- end
26
-
27
- it "all arrays contain only chunks" do
28
- expect(chunks.values.flat_map { |i| i.map &:class }.uniq).to eq [LastPass::Chunk]
29
- end
30
-
31
- it "all chunks grouped under correct IDs" do
32
- expect(
33
- chunks.all? { |id, chunk_group| chunk_group.map(&:id).uniq == [id] }
34
- ).to be_true
98
+ it "all values are instances of Chunk" do
99
+ expect(chunks.map(&:class).uniq).to eq [LastPass::Chunk]
35
100
  end
36
101
  end
37
102
  end
38
103
 
39
- describe ".parse_account" do
104
+ describe ".parse_ACCT" do
40
105
  let(:accounts) {
41
106
  LastPass::Parser
42
- .extract_chunks(blob)["ACCT"]
43
- .map { |i| LastPass::Parser.parse_account i, TEST_ENCRYPTION_KEY }
107
+ .extract_chunks(blob)
108
+ .select { |i| i.id == "ACCT" }
109
+ .map { |i| LastPass::Parser.parse_ACCT i, TEST_ENCRYPTION_KEY }
44
110
  }
45
111
 
46
112
  it "parses accounts" do
@@ -48,9 +114,80 @@ describe LastPass::Parser do
48
114
  end
49
115
  end
50
116
 
117
+ describe ".parse_PRIK" do
118
+ let(:chunk) { LastPass::Chunk.new "PRIK", encoded_rsa_key }
119
+ let(:rsa_key) { LastPass::Parser.parse_PRIK chunk, rsa_key_encryption_key }
120
+
121
+ it "parses private key" do
122
+ expect(rsa_key).to be_instance_of OpenSSL::PKey::RSA
123
+ expect(rsa_key.n.to_s).to eq "26119467519435514320618523953258926539081857789201" +
124
+ "11592360794055150234493177840791445076164320959092" +
125
+ "33977645519805962686071307052774013402170389235283" +
126
+ "48398581900094955608774421569689169697285847986479" +
127
+ "82303230642077254435741682688235176460351551099267" +
128
+ "22581481667367599195203736002065084704013295528661" +
129
+ "76687143747593851140122182044652173598693510643390" +
130
+ "47711449981712845835960707676646864765530616733341" +
131
+ "58401920829305659156984748726238485655720031774127" +
132
+ "01900577710668575227691993026576480667273922300137" +
133
+ "80405264300989392980537603337301835174777026188388" +
134
+ "93147718435999645840214854231168704372464234421315" +
135
+ "01138217872658041"
136
+ expect(rsa_key.e.to_s).to eq "65537"
137
+ expect(rsa_key.d.to_s).to eq "20217010678828834626882766446083366137418639853408" +
138
+ "07494174069610076841252047428625473158347002598408" +
139
+ "18346644251082549844764624454370315666751565294997" +
140
+ "10533208173186395672159239558808345075823110774221" +
141
+ "61501075434955107584446470508660844962452555542861" +
142
+ "72030926355197158923586674949673551608716945271868" +
143
+ "18816984671497443384191412119383687600754285611808" +
144
+ "23265620694961977962255376280640334543711420731809" +
145
+ "16169692928898605559361322123131373948352054888316" +
146
+ "99068010065680008419210277574874665723796199239285" +
147
+ "78432149273871356528827780412288057677598714485872" +
148
+ "23380715275000339748138416696881866569449168516354" +
149
+ "08203050733598637"
150
+ expect(rsa_key.p.to_s).to eq "17745924258106322606344019888040076543466707208121" +
151
+ "93651272762195900747632457567234817364256394944312" +
152
+ "33791510564351470780224344194760390006214095043405" +
153
+ "42496712265086317539172843039592265661675784866722" +
154
+ "91261262550895476526939878375016658686669778355984" +
155
+ "43725100552628219407700007375820870959681331890216" +
156
+ "873285999"
157
+ expect(rsa_key.q.to_s).to eq "14718572636476888213359534581670909910031809536407" +
158
+ "40164297606657861988206326322941728093846078102409" +
159
+ "77115817405984843964689092056948880068086594283588" +
160
+ "67786990898525462713620707076259988063113810297786" +
161
+ "62342502396556461808879680749106840152602791951788" +
162
+ "07295572399572445909627940220804206538364578785262" +
163
+ "498615959"
164
+ expect(rsa_key.dmp1.to_s).to eq "11323089471614997519408698592522878386531994069" +
165
+ "33541387540978328974191124807026398192741826901" +
166
+ "86286081197790519393403018396347119829946883285" +
167
+ "08800265628051101161010033119239372833462468119" +
168
+ "90625594353955836736745514688525978377008530625" +
169
+ "69694172942783772849726563761756732407513441791" +
170
+ "680438851248236159711158591"
171
+ expect(rsa_key.dmq1.to_s).to eq "12614892732210916138126631634839174964470249502" +
172
+ "72370951196981338360130575847987543477227082933" +
173
+ "41184913630399067613236576233063778305668453307" +
174
+ "65828324726545238243590265660986543730618177968" +
175
+ "24851190055502445616363498122584261892788460430" +
176
+ "15963041982287770355559480659540210015737708509" +
177
+ "273864533597668007301940253"
178
+ expect(rsa_key.iqmp.to_s).to eq "12662716333617943892704787530332782239196594580" +
179
+ "72960727418453194230165281227127897455330083723" +
180
+ "88895713617946267318745745224382578970891647971" +
181
+ "94015463887039228876036602797561671319853126600" +
182
+ "52663805817336717151173320411542486382434841161" +
183
+ "62999647203566877832873138065626190040996517274" +
184
+ "418161068665712298519808863"
185
+ end
186
+ end
187
+
51
188
  describe ".read_chunk" do
52
189
  it "returns a chunk" do
53
- with_hex "4142434400000004DEADBEEF" + padding do |io|
190
+ with_chunk_hex "ABCD", "DEADBEEF", padding do |io|
54
191
  expect(LastPass::Parser.read_chunk io).to eq LastPass::Chunk.new("ABCD", "DEADBEEF".decode_hex)
55
192
  expect(io.pos).to eq 12
56
193
  end
@@ -59,7 +196,7 @@ describe LastPass::Parser do
59
196
 
60
197
  describe ".read_item" do
61
198
  it "returns an item" do
62
- with_hex "00000004DEADBEEF" + padding do |io|
199
+ with_item_hex "DEADBEEF", padding do |io|
63
200
  expect(LastPass::Parser.read_item io).to eq "DEADBEEF".decode_hex
64
201
  expect(io.pos).to eq 8
65
202
  end
@@ -68,14 +205,14 @@ describe LastPass::Parser do
68
205
 
69
206
  describe ".skip_item" do
70
207
  it "skips an empty item" do
71
- with_hex "00000000" + padding do |io|
208
+ with_item_hex "", padding do |io|
72
209
  LastPass::Parser.skip_item io
73
210
  expect(io.pos).to eq 4
74
211
  end
75
212
  end
76
213
 
77
214
  it "skips a non-empty item" do
78
- with_hex "00000004DEADBEEF" + padding do |io|
215
+ with_item_hex "DEADBEEF", padding do |io|
79
216
  LastPass::Parser.skip_item io
80
217
  expect(io.pos).to eq 8
81
218
  end
@@ -293,4 +430,44 @@ describe LastPass::Parser do
293
430
  yield io
294
431
  end
295
432
  end
433
+
434
+ #
435
+ # Chunks
436
+ #
437
+
438
+ def with_chunk id, payload, padding = "", &block
439
+ with_bytes make_chunk(id, payload, padding), &block
440
+ end
441
+
442
+ def with_chunk_hex id, payload, padding = "", &block
443
+ with_bytes make_chunk_hex(id, payload, padding), &block
444
+ end
445
+
446
+ def make_chunk id, payload, padding = ""
447
+ [id, payload.size, payload, padding].pack "a4Na*a*"
448
+ end
449
+
450
+ def make_chunk_hex id, payload, padding = ""
451
+ make_chunk id, payload.decode_hex, padding
452
+ end
453
+
454
+ #
455
+ # Items
456
+ #
457
+
458
+ def with_item payload, padding = "", &block
459
+ with_bytes make_item(payload, padding), &block
460
+ end
461
+
462
+ def with_item_hex payload, padding = "", &block
463
+ with_bytes make_item_hex(payload, padding), &block
464
+ end
465
+
466
+ def make_item payload, padding = ""
467
+ [payload.size, payload, padding].pack "Na*a*"
468
+ end
469
+
470
+ def make_item_hex payload, padding = ""
471
+ make_item payload.decode_hex, padding
472
+ end
296
473
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lastpass
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-12-15 00:00:00.000000000 Z
12
+ date: 2014-03-16 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: httparty
@@ -18,7 +18,7 @@ dependencies:
18
18
  requirements:
19
19
  - - ~>
20
20
  - !ruby/object:Gem::Version
21
- version: 0.12.0
21
+ version: 0.13.0
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - ~>
28
28
  - !ruby/object:Gem::Version
29
- version: 0.12.0
29
+ version: 0.13.0
30
30
  - !ruby/object:Gem::Dependency
31
31
  name: pbkdf2-ruby
32
32
  requirement: !ruby/object:Gem::Requirement
@@ -50,7 +50,7 @@ dependencies:
50
50
  requirements:
51
51
  - - ~>
52
52
  - !ruby/object:Gem::Version
53
- version: 10.0.0
53
+ version: 10.1.0
54
54
  type: :development
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
@@ -58,7 +58,7 @@ dependencies:
58
58
  requirements:
59
59
  - - ~>
60
60
  - !ruby/object:Gem::Version
61
- version: 10.0.0
61
+ version: 10.1.0
62
62
  - !ruby/object:Gem::Dependency
63
63
  name: rspec
64
64
  requirement: !ruby/object:Gem::Requirement
@@ -106,6 +106,7 @@ files:
106
106
  - Makefile
107
107
  - README.md
108
108
  - Rakefile
109
+ - TODO
109
110
  - example/credentials.yaml.example
110
111
  - example/example.rb
111
112
  - lastpass.gemspec