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 +6 -0
- data/README.md +10 -1
- data/TODO +5 -0
- data/example/example.rb +19 -2
- data/lastpass.gemspec +2 -2
- data/lib/lastpass/exceptions.rb +10 -4
- data/lib/lastpass/fetcher.rb +31 -19
- data/lib/lastpass/parser.rb +56 -5
- data/lib/lastpass/vault.rb +21 -6
- data/lib/lastpass/version.rb +1 -1
- data/spec/fetcher_spec.rb +68 -14
- data/spec/parser_spec.rb +201 -24
- metadata +7 -6
data/CHANGELOG.md
CHANGED
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
|
-
|
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.
|
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.
|
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
|
|
data/lib/lastpass/exceptions.rb
CHANGED
@@ -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
|
16
|
+
class InvalidResponseError < Error; end
|
17
17
|
|
18
18
|
# Server responded with XML we don't understand
|
19
|
-
class
|
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
|
26
|
+
class LastPassUnknownUsernameError < Error; end
|
27
27
|
|
28
28
|
# LastPass error: invalid password
|
29
|
-
class
|
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
|
data/lib/lastpass/fetcher.rb
CHANGED
@@ -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
|
30
|
+
raise InvalidResponseError, "Key iteration count is invalid"
|
31
31
|
end
|
32
32
|
|
33
|
-
raise
|
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,
|
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
|
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
|
82
|
+
return UnknownResponseSchemaError unless error.is_a? Hash
|
74
83
|
|
75
84
|
exceptions = {
|
76
|
-
"unknownemail" =>
|
77
|
-
"unknownpassword" =>
|
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
|
-
|
98
|
+
InvalidResponseError.new message
|
87
99
|
end
|
88
100
|
end
|
89
101
|
|
data/lib/lastpass/parser.rb
CHANGED
@@ -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 =
|
11
|
+
chunks = []
|
9
12
|
|
10
13
|
StringIO.open blob.bytes do |stream|
|
11
14
|
while !stream.eof?
|
12
|
-
|
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
|
-
|
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,
|
data/lib/lastpass/vault.rb
CHANGED
@@ -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
|
-
|
31
|
-
|
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
|
data/lib/lastpass/version.rb
CHANGED
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::
|
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::
|
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::
|
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
|
-
|
63
|
-
|
64
|
-
|
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,
|
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::
|
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::
|
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::
|
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::
|
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::
|
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::
|
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
|
96
|
+
it { expect(chunks).to be_instance_of Array }
|
18
97
|
|
19
|
-
it "all
|
20
|
-
expect(chunks.
|
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 ".
|
104
|
+
describe ".parse_ACCT" do
|
40
105
|
let(:accounts) {
|
41
106
|
LastPass::Parser
|
42
|
-
.extract_chunks(blob)
|
43
|
-
.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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:
|
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.
|
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.
|
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.
|
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.
|
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
|