lastpass 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ # Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
2
+ # Licensed under the terms of the MIT license. See LICENCE for details.
3
+
4
+ module LastPass
5
+ class Session
6
+ attr_reader :id,
7
+ :key_iteration_count
8
+
9
+ def initialize id, key_iteration_count
10
+ @id = id
11
+ @key_iteration_count = key_iteration_count
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,34 @@
1
+ # Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
2
+ # Licensed under the terms of the MIT license. See LICENCE for details.
3
+
4
+ module LastPass
5
+ class Vault
6
+ attr_reader :accounts
7
+
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
11
+ end
12
+
13
+ # Creates a vault from a locally stored blob
14
+ def self.open_local blob_filename, username, password
15
+ # TODO: read the blob here
16
+ end
17
+
18
+ # Creates a vault from a blob object
19
+ def self.open blob, username, password
20
+ new blob, blob.encryption_key(username, password)
21
+ end
22
+
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
26
+ end
27
+
28
+ # This more of an internal method, use of the static constructors instead
29
+ def initialize blob, encryption_key
30
+ chunks = Parser.extract_chunks blob
31
+ @accounts = (chunks["ACCT"] || []).map { |i| Parser.parse_account i, encryption_key }
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,6 @@
1
+ # Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
2
+ # Licensed under the terms of the MIT license. See LICENCE for details.
3
+
4
+ module LastPass
5
+ VERSION = "1.0.0"
6
+ end
@@ -0,0 +1,22 @@
1
+ # Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
2
+ # Licensed under the terms of the MIT license. See LICENCE for details.
3
+
4
+ require "spec_helper"
5
+
6
+ describe LastPass::Account do
7
+ let(:id) { "id" }
8
+ let(:name) { "name" }
9
+ let(:username) { "username" }
10
+ let(:password) { "password" }
11
+ let(:url) { "url" }
12
+ let(:group) { "group" }
13
+
14
+ subject { LastPass::Account.new id, name, username, password, url, group }
15
+
16
+ its(:id) { should eq id }
17
+ its(:name) { should eq name }
18
+ its(:username) { should eq username }
19
+ its(:password) { should eq password }
20
+ its(:url) { should eq url }
21
+ its(:group) { should eq group }
22
+ end
@@ -0,0 +1,23 @@
1
+ # Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
2
+ # Licensed under the terms of the MIT license. See LICENCE for details.
3
+
4
+ require "spec_helper"
5
+
6
+ describe LastPass::Blob do
7
+ let(:bytes) { "TFBBVgAAAAMxMjJQUkVNAAAACjE0MTQ5".decode64 }
8
+ let(:key_iteration_count) { 500 }
9
+ let(:username) { "postlass@gmail.com" }
10
+ let(:password) { "pl1234567890" }
11
+ let(:encryption_key) { "OfOUvVnQzB4v49sNh4+PdwIFb9Fr5+jVfWRTf+E2Ghg=".decode64 }
12
+
13
+ subject { LastPass::Blob.new bytes, key_iteration_count }
14
+
15
+ its(:bytes) { should eq bytes }
16
+ its(:key_iteration_count) { should eq key_iteration_count }
17
+
18
+ describe "#encryption_key" do
19
+ it "returns encryption key" do
20
+ expect(subject.encryption_key username, password).to eq encryption_key
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,14 @@
1
+ # Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
2
+ # Licensed under the terms of the MIT license. See LICENCE for details.
3
+
4
+ require "spec_helper"
5
+
6
+ describe LastPass::Chunk do
7
+ let(:id) { "IDID" }
8
+ let(:payload) { "Payload" }
9
+
10
+ subject { LastPass::Chunk.new id, payload }
11
+
12
+ its(:id) { should eq id }
13
+ its(:payload) { should eq payload }
14
+ end
@@ -0,0 +1,228 @@
1
+ # Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
2
+ # Licensed under the terms of the MIT license. See LICENCE for details.
3
+
4
+ require "spec_helper"
5
+
6
+ describe LastPass::Fetcher do
7
+ let(:username) { "username" }
8
+ let(:password) { "password" }
9
+ let(:key_iteration_count) { 5000 }
10
+ let(:session_id) { "53ru,Hb713QnEVM5zWZ16jMvxS0" }
11
+ let(:session) { LastPass::Session.new session_id, key_iteration_count }
12
+ let(:blob_response) { "TFBBVgAAAAMxMjJQUkVNAAAACjE0MTQ5" }
13
+ let(:blob_bytes) { blob_response.decode64 }
14
+ let(:blob) { LastPass::Blob.new blob_bytes, key_iteration_count }
15
+
16
+ describe ".request_iteration_count" do
17
+ it "makes a POST request" do
18
+ expect(web_client = double("web_client")).to receive(:post)
19
+ .with("https://lastpass.com/iterations.php", query: {email: username})
20
+ .and_return(http_ok(key_iteration_count.to_s))
21
+
22
+ LastPass::Fetcher.request_iteration_count username, web_client
23
+ end
24
+
25
+ it "returns key iteration count" do
26
+ expect(
27
+ LastPass::Fetcher.request_iteration_count username,
28
+ double("web_client", post: http_ok(key_iteration_count.to_s))
29
+ ).to eq key_iteration_count
30
+ end
31
+
32
+ it "raises an exception on HTTP error" do
33
+ expect {
34
+ LastPass::Fetcher.request_iteration_count username,
35
+ double("web_client", post: http_error)
36
+ }.to raise_error LastPass::NetworkError
37
+ end
38
+
39
+ it "raises an exception on invalid key iteration count" do
40
+ expect {
41
+ LastPass::Fetcher.request_iteration_count username,
42
+ double("web_client", post: http_ok("not a number"))
43
+ }.to raise_error LastPass::InvalidResponse, "Key iteration count is invalid"
44
+ end
45
+
46
+ it "raises an exception on zero key iteration count" do
47
+ expect {
48
+ LastPass::Fetcher.request_iteration_count username,
49
+ double("web_client", post: http_ok("0"))
50
+ }.to raise_error LastPass::InvalidResponse, "Key iteration count is not positive"
51
+ end
52
+
53
+ it "raises an exception on negative key iteration count" do
54
+ expect {
55
+ LastPass::Fetcher.request_iteration_count username,
56
+ double("web_client", post: http_ok("-1"))
57
+ }.to raise_error LastPass::InvalidResponse, "Key iteration count is not positive"
58
+ end
59
+ end
60
+
61
+ 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)
65
+ .and_return(http_ok("ok" => {"sessionid" => session_id}))
66
+
67
+ LastPass::Fetcher.request_login username, password, key_iteration_count, web_client
68
+ end
69
+
70
+ it "returns a session" do
71
+ expect(request_login_with_xml "<ok sessionid='#{session_id}' />").to eq session
72
+ end
73
+
74
+ it "raises an exception on HTTP error" do
75
+ expect { request_login_with_error }.to raise_error LastPass::NetworkError
76
+ end
77
+
78
+ 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
80
+ end
81
+
82
+ it "raises an exception on unknown response schema" do
83
+ expect { request_login_with_xml "<unknown />" }.to raise_error LastPass::UnknownResponseSchema
84
+ end
85
+
86
+ it "raises an exception on unknown response schema" do
87
+ expect { request_login_with_xml "<response />" }.to raise_error LastPass::UnknownResponseSchema
88
+ end
89
+
90
+ it "raises an exception on unknown response schema" do
91
+ expect { request_login_with_xml "<response><error /></response>" }
92
+ .to raise_error LastPass::UnknownResponseSchema
93
+ end
94
+
95
+ it "raises an exception on unknown username" do
96
+ message = "Unknown email address."
97
+ expect { request_login_with_lastpass_error "unknownemail", message }
98
+ .to raise_error LastPass::LastPassUnknownUsername, message
99
+ end
100
+
101
+ it "raises an exception on invalid password" do
102
+ message = "Invalid password!"
103
+ expect { request_login_with_lastpass_error "unknownpassword", message }
104
+ .to raise_error LastPass::LastPassInvalidPassword, message
105
+ end
106
+
107
+ it "raises an exception on unknown LastPass error with a message" do
108
+ message = "Unknow error message"
109
+ expect { request_login_with_lastpass_error "Unknown cause", message }
110
+ .to raise_error LastPass::LastPassUnknownError, message
111
+ end
112
+
113
+ it "raises an exception on unknown LastPass error without a message" do
114
+ cause = "Unknown casue"
115
+ expect { request_login_with_lastpass_error cause }
116
+ .to raise_error LastPass::LastPassUnknownError, cause
117
+ end
118
+ end
119
+
120
+ describe ".fetch" do
121
+ it "makes a GET request" do
122
+ expect(web_client = double("web_client")).to receive(:get)
123
+ .with("https://lastpass.com/getaccts.php?mobile=1&b64=1&hash=0.0",
124
+ format: :plain,
125
+ cookies: {"PHPSESSID" => session_id})
126
+ .and_return(http_ok(blob_response))
127
+
128
+ LastPass::Fetcher.fetch session, web_client
129
+ end
130
+
131
+ it "returns a blob" do
132
+ expect(LastPass::Fetcher.fetch session, double("web_client", get: http_ok(blob_response)))
133
+ .to eq blob
134
+ end
135
+
136
+ it "raises an exception on HTTP error" do
137
+ expect {
138
+ LastPass::Fetcher.fetch session, double("web_client", get: http_error)
139
+ } .to raise_error LastPass::NetworkError
140
+ end
141
+ end
142
+
143
+ describe ".make_key" do
144
+ it "generates correct keys" do
145
+ def key iterations
146
+ LastPass::Fetcher.make_key "postlass@gmail.com", "pl1234567890", iterations
147
+ end
148
+
149
+ expect(key 1).to eq "C/Bh2SGWxI8JDu54DbbpV8J9wa6pKbesIb9MAXkeF3Y=".decode64
150
+ expect(key 5).to eq "pE9goazSCRqnWwcixWM4NHJjWMvB5T15dMhe6ug1pZg=".decode64
151
+ expect(key 10).to eq "n9S0SyJdrMegeBHtkxUx8Lzc7wI6aGl+y3/udGmVey8=".decode64
152
+ expect(key 50).to eq "GwI8/kNy1NjIfe3Z0VAZfF78938UVuCi6xAL3MJBux0=".decode64
153
+ expect(key 100).to eq "piGdSULeHMWiBS3QJNM46M5PIYwQXA6cNS10pLB3Xf8=".decode64
154
+ expect(key 500).to eq "OfOUvVnQzB4v49sNh4+PdwIFb9Fr5+jVfWRTf+E2Ghg=".decode64
155
+ expect(key 1000).to eq "z7CdwlIkbu0XvcB7oQIpnlqwNGemdrGTBmDKnL9taPg=".decode64
156
+ end
157
+ end
158
+
159
+ describe ".make_hash" do
160
+ it "generates correct hashes" do
161
+ def hash iterations
162
+ LastPass::Fetcher.make_hash "postlass@gmail.com", "pl1234567890", iterations
163
+ end
164
+
165
+ expect(hash 1).to eq "a1943cfbb75e37b129bbf78b9baeab4ae6dd08225776397f66b8e0c7a913a055"
166
+ expect(hash 5).to eq "a95849e029a7791cfc4503eed9ec96ab8675c4a7c4e82b00553ddd179b3d8445"
167
+ expect(hash 10).to eq "0da0b44f5e6b7306f14e92de6d629446370d05afeb1dc07cfcbe25f169170c16"
168
+ expect(hash 50).to eq "1d5bc0d636da4ad469cefe56c42c2ff71589facb9c83f08fcf7711a7891cc159"
169
+ expect(hash 100).to eq "82fc12024acb618878ba231a9948c49c6f46e30b5a09c11d87f6d3338babacb5"
170
+ expect(hash 500).to eq "3139861ae962801b59fc41ff7eeb11f84ca56d810ab490f0d8c89d9d9ab07aa6"
171
+ expect(hash 1000).to eq "03161354566c396fcd624a424164160e890e96b4b5fa6d942fc6377ab613513b"
172
+ end
173
+ end
174
+
175
+ #
176
+ # Helpers
177
+ #
178
+
179
+ private
180
+
181
+ def mock_response type, code, body
182
+ double response: type.new("1.1", code, ""),
183
+ parsed_response: body
184
+ end
185
+
186
+ def http_ok body
187
+ mock_response Net::HTTPOK, 200, body
188
+ end
189
+
190
+ def http_error body = ""
191
+ mock_response Net::HTTPNotFound, 404, body
192
+ end
193
+
194
+ def xml text
195
+ MultiXml.parse text
196
+ end
197
+
198
+ def lastpass_error cause, message
199
+ if message
200
+ %Q{<response><error cause="#{cause}" message="#{message}" /></response>}
201
+ else
202
+ %Q{<response><error cause="#{cause}" /></response>}
203
+ end
204
+ end
205
+
206
+ def request_login_with_lastpass_error cause, message = nil
207
+ request_login_with_xml lastpass_error cause, message
208
+ end
209
+
210
+ def request_login_with_xml text
211
+ request_login_with_ok xml text
212
+ end
213
+
214
+ def request_login_with_ok response
215
+ request_login_with_response http_ok response
216
+ end
217
+
218
+ def request_login_with_error
219
+ request_login_with_response http_error
220
+ end
221
+
222
+ def request_login_with_response response
223
+ LastPass::Fetcher.request_login username,
224
+ password,
225
+ key_iteration_count,
226
+ double("web_client", post: response)
227
+ end
228
+ end
@@ -0,0 +1,292 @@
1
+ # Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
2
+ # Licensed under the terms of the MIT license. See LICENCE for details.
3
+
4
+ require "spec_helper"
5
+ require_relative "test_data"
6
+
7
+ describe LastPass::Parser do
8
+ let(:key_iteration_count) { 5000 }
9
+ let(:blob) { LastPass::Blob.new TEST_BLOB, key_iteration_count }
10
+ let(:padding) { "BEEFFACE"}
11
+ let(:encryption_key) { "OfOUvVnQzB4v49sNh4+PdwIFb9Fr5+jVfWRTf+E2Ghg=".decode64 }
12
+
13
+ describe ".extract_chunks" do
14
+ context "returned chunks" do
15
+ let(:chunks) { LastPass::Parser.extract_chunks blob }
16
+
17
+ it { expect(chunks).to be_instance_of Hash }
18
+
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
35
+ end
36
+ end
37
+ end
38
+
39
+ describe ".parse_account" do
40
+ let(:accounts) {
41
+ LastPass::Parser
42
+ .extract_chunks(blob)["ACCT"]
43
+ .map { |i| LastPass::Parser.parse_account i, TEST_ENCRYPTION_KEY }
44
+ }
45
+
46
+ it "parses accounts" do
47
+ expect(accounts.map &:id).to eq TEST_ACCOUNTS.map &:id
48
+ end
49
+ end
50
+
51
+ describe ".read_chunk" do
52
+ it "returns a chunk" do
53
+ with_hex "4142434400000004DEADBEEF" + padding do |io|
54
+ expect(LastPass::Parser.read_chunk io).to eq LastPass::Chunk.new("ABCD", "DEADBEEF".decode_hex)
55
+ expect(io.pos).to eq 12
56
+ end
57
+ end
58
+ end
59
+
60
+ describe ".read_item" do
61
+ it "returns an item" do
62
+ with_hex "00000004DEADBEEF" + padding do |io|
63
+ expect(LastPass::Parser.read_item io).to eq "DEADBEEF".decode_hex
64
+ expect(io.pos).to eq 8
65
+ end
66
+ end
67
+ end
68
+
69
+ describe ".skip_item" do
70
+ it "skips an empty item" do
71
+ with_hex "00000000" + padding do |io|
72
+ LastPass::Parser.skip_item io
73
+ expect(io.pos).to eq 4
74
+ end
75
+ end
76
+
77
+ it "skips a non-empty item" do
78
+ with_hex "00000004DEADBEEF" + padding do |io|
79
+ LastPass::Parser.skip_item io
80
+ expect(io.pos).to eq 8
81
+ end
82
+ end
83
+ end
84
+
85
+ describe ".read_id" do
86
+ it "returns an id" do
87
+ with_bytes "ABCD" + padding do |io|
88
+ expect(LastPass::Parser.read_id io).to eq "ABCD"
89
+ expect(io.pos).to eq 4
90
+ end
91
+ end
92
+ end
93
+
94
+ describe ".read_size" do
95
+ it "returns a size" do
96
+ with_hex "DEADBEEF" + padding do |io|
97
+ expect(LastPass::Parser.read_size io).to eq 0xDEADBEEF
98
+ expect(io.pos).to eq 4
99
+ end
100
+ end
101
+ end
102
+
103
+ describe ".read_payload" do
104
+ it "returns a payload" do
105
+ with_hex "FEEDDEADBEEF" + padding do |io|
106
+ expect(LastPass::Parser.read_payload io, 6).to eq "FEEDDEADBEEF".decode_hex
107
+ expect(io.pos).to eq 6
108
+ end
109
+ end
110
+ end
111
+
112
+ describe ".read_uint32" do
113
+ it "returns a number" do
114
+ with_hex "DEADBEEF" + padding do |io|
115
+ expect(LastPass::Parser.read_size io).to eq 0xDEADBEEF
116
+ expect(io.pos).to eq 4
117
+ end
118
+ end
119
+ end
120
+
121
+ describe ".decode_hex" do
122
+ it "decodes hex" do
123
+ expect(LastPass::Parser.decode_hex "")
124
+ .to eq ""
125
+
126
+ expect(LastPass::Parser.decode_hex "00ff")
127
+ .to eq "\x00\xFF"
128
+
129
+ expect(LastPass::Parser.decode_hex "00010203040506070809")
130
+ .to eq "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09"
131
+
132
+ expect(LastPass::Parser.decode_hex "000102030405060708090a0b0c0d0e0f")
133
+ .to eq "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"
134
+
135
+ expect(LastPass::Parser.decode_hex "8af633933e96a3c3550c2734bd814195")
136
+ .to eq "\x8A\xF6\x33\x93\x3E\x96\xA3\xC3\x55\x0C\x27\x34\xBD\x81\x41\x95"
137
+ end
138
+
139
+ it "raises exception on odd length" do
140
+ expect { LastPass::Parser.decode_hex "0" }
141
+ .to raise_error ArgumentError, "Input length must be multple of 2"
142
+ end
143
+
144
+ it "raises exception on invalid characters" do
145
+ expect { LastPass::Parser.decode_hex "xz" }
146
+ .to raise_error ArgumentError, "Input contains invalid characters"
147
+ end
148
+ end
149
+
150
+ describe ".decode_base64" do
151
+ it "decodes base64" do
152
+ def check base64, plain
153
+ expect(LastPass::Parser.decode_base64 base64).to eq plain
154
+ end
155
+
156
+ check "", ""
157
+ check "YQ==", "a"
158
+ check "YWI=", "ab"
159
+ check "YWJj", "abc"
160
+ check "YWJjZA==", "abcd"
161
+ end
162
+ end
163
+
164
+ describe ".decode_aes256_auto" do
165
+ def check encoded, decoded
166
+ expect(LastPass::Parser.decode_aes256_auto encoded, encryption_key)
167
+ .to eq decoded
168
+ end
169
+
170
+ it "decodes a blank string" do
171
+ check "", ""
172
+ end
173
+
174
+ it "decodes ECB/plain string" do
175
+ check "BNhd3Q3ZVODxk9c0C788NUPTIfYnZuxXfkghtMJ8jVM=".decode64,
176
+ "All your base are belong to us"
177
+ end
178
+
179
+ it "decodes ECB/base64 string" do
180
+ check "BNhd3Q3ZVODxk9c0C788NUPTIfYnZuxXfkghtMJ8jVM=",
181
+ "All your base are belong to us"
182
+ end
183
+
184
+ it "decodes CBC/plain string" do
185
+ check "IcokDWmjOkKtLpZehWKL6666Uj6fNXPpX6lLWlou+1Lrwb+D3ymP6BAwd6C0TB3hSA==".decode64,
186
+ "All your base are belong to us"
187
+ end
188
+
189
+ it "decodes CBC/base64 string" do
190
+ check "!YFuiAVZgOD2K+s6y8yaMOw==|TZ1+if9ofqRKTatyUaOnfudletslMJ/RZyUwJuR/+aI=",
191
+ "All your base are belong to us"
192
+ end
193
+ end
194
+
195
+ describe ".decode_aes256_ecb_plain" do
196
+ def check encoded, decoded
197
+ expect(LastPass::Parser.decode_aes256_ecb_plain encoded.decode64, encryption_key)
198
+ .to eq decoded
199
+ end
200
+
201
+ it "decodes a blank string" do
202
+ check "", ""
203
+ end
204
+
205
+ it "decodes a short string" do
206
+ check "8mHxIA8rul6eq72a/Gq2iw==", "0123456789"
207
+ end
208
+
209
+ it "decodes a long string" do
210
+ check "BNhd3Q3ZVODxk9c0C788NUPTIfYnZuxXfkghtMJ8jVM=", "All your base are belong to us"
211
+ end
212
+ end
213
+
214
+ describe ".decode_aes256_ecb_base64" do
215
+ def check encoded, decoded
216
+ expect(LastPass::Parser.decode_aes256_ecb_base64 encoded, encryption_key)
217
+ .to eq decoded
218
+ end
219
+
220
+ it "decodes a blank string" do
221
+ check "", ""
222
+ end
223
+
224
+ it "decodes a short string" do
225
+ check "8mHxIA8rul6eq72a/Gq2iw==", "0123456789"
226
+ end
227
+
228
+ it "decodes a long string" do
229
+ check "BNhd3Q3ZVODxk9c0C788NUPTIfYnZuxXfkghtMJ8jVM=", "All your base are belong to us"
230
+ end
231
+ end
232
+
233
+ describe ".decode_aes256_cbc_plain" do
234
+ def check encoded, decoded
235
+ expect(LastPass::Parser.decode_aes256_cbc_plain encoded.decode64, encryption_key)
236
+ .to eq decoded
237
+ end
238
+
239
+ it "decodes a blank string" do
240
+ check "", ""
241
+ end
242
+
243
+ it "decodes a short string" do
244
+ check "IQ+hiIy0vGG4srsHmXChe3ehWc/rYPnfiyqOG8h78DdX", "0123456789"
245
+ end
246
+
247
+ it "decodes a long string" do
248
+ check "IcokDWmjOkKtLpZehWKL6666Uj6fNXPpX6lLWlou+1Lrwb+D3ymP6BAwd6C0TB3hSA==",
249
+ "All your base are belong to us"
250
+ end
251
+ end
252
+
253
+ describe ".decode_aes256_cbc_base64" do
254
+ def check encoded, decoded
255
+ expect(LastPass::Parser.decode_aes256_cbc_base64 encoded, encryption_key)
256
+ .to eq decoded
257
+ end
258
+
259
+ it "decodes a blank string" do
260
+ check "", ""
261
+ end
262
+
263
+ it "decodes a short string" do
264
+ check "!6TZb9bbrqpocMaNgFjrhjw==|f7RcJ7UowesqGk+um+P5ug==", "0123456789"
265
+ end
266
+
267
+ it "decodes a long string" do
268
+ check "!YFuiAVZgOD2K+s6y8yaMOw==|TZ1+if9ofqRKTatyUaOnfudletslMJ/RZyUwJuR/+aI=",
269
+ "All your base are belong to us"
270
+ end
271
+ end
272
+
273
+ #
274
+ # Helpers
275
+ #
276
+
277
+ private
278
+
279
+ def with_blob &block
280
+ with_bytes TEST_BLOB, &block
281
+ end
282
+
283
+ def with_hex hex, &block
284
+ with_bytes hex.decode_hex, &block
285
+ end
286
+
287
+ def with_bytes bytes, &block
288
+ StringIO.open bytes do |io|
289
+ yield io
290
+ end
291
+ end
292
+ end