lastpass 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|