diff options
author | Mike Crute <mcrute@gmail.com> | 2016-11-26 14:43:59 -0800 |
---|---|---|
committer | Mike Crute <mcrute@gmail.com> | 2016-11-26 14:43:59 -0800 |
commit | b96e751c8944c1b63bf78033c76892c72abad432 (patch) | |
tree | e0bda7ce52d88be32294acb0e8d55ce481984fe4 | |
download | go-1password-b96e751c8944c1b63bf78033c76892c72abad432.tar.bz2 go-1password-b96e751c8944c1b63bf78033c76892c72abad432.tar.xz go-1password-b96e751c8944c1b63bf78033c76892c72abad432.zip |
Initial import
-rw-r--r-- | main.go | 446 |
1 files changed, 446 insertions, 0 deletions
@@ -0,0 +1,446 @@ | |||
1 | package main | ||
2 | |||
3 | import ( | ||
4 | "bytes" | ||
5 | "crypto/aes" | ||
6 | "crypto/cipher" | ||
7 | "crypto/hmac" | ||
8 | "crypto/sha256" | ||
9 | "crypto/sha512" | ||
10 | "encoding/base64" | ||
11 | "encoding/binary" | ||
12 | "encoding/json" | ||
13 | "errors" | ||
14 | "fmt" | ||
15 | "io/ioutil" | ||
16 | "os/user" | ||
17 | "path" | ||
18 | "strings" | ||
19 | "syscall" | ||
20 | "time" | ||
21 | |||
22 | "golang.org/x/crypto/pbkdf2" | ||
23 | "golang.org/x/crypto/ssh/terminal" | ||
24 | ) | ||
25 | |||
26 | var Categories = map[string]string{ | ||
27 | "Login": "001", | ||
28 | "Credit Card": "002", | ||
29 | "Secure Note": "003", | ||
30 | "Identity": "004", | ||
31 | "Password": "005", | ||
32 | "Tombstone": "099", | ||
33 | "Software License": "100", | ||
34 | "Bank Account": "101", | ||
35 | "Database": "102", | ||
36 | "Driver License": "103", | ||
37 | "Outdoor License": "104", | ||
38 | "Membership": "105", | ||
39 | "Passport": "106", | ||
40 | "Rewards": "107", | ||
41 | "SSN": "108", | ||
42 | "Router": "109", | ||
43 | "Server": "110", | ||
44 | "Email": "111", | ||
45 | } | ||
46 | |||
47 | var CategoriesRev = map[string]string{ | ||
48 | "001": "Login", | ||
49 | "002": "Credit Card", | ||
50 | "003": "Secure Note", | ||
51 | "004": "Identity", | ||
52 | "005": "Password", | ||
53 | "099": "Tombstone", | ||
54 | "100": "Software License", | ||
55 | "101": "Bank Account", | ||
56 | "102": "Database", | ||
57 | "103": "Driver License", | ||
58 | "104": "Outdoor License", | ||
59 | "105": "Membership", | ||
60 | "106": "Passport", | ||
61 | "107": "Rewards", | ||
62 | "108": "SSN", | ||
63 | "109": "Router", | ||
64 | "110": "Server", | ||
65 | "111": "Email", | ||
66 | } | ||
67 | |||
68 | type Key struct { | ||
69 | Encryption []byte | ||
70 | Mac []byte | ||
71 | } | ||
72 | |||
73 | func NewKey(combined []byte) *Key { | ||
74 | if len(combined) != 64 { | ||
75 | panic("Invalid key size") | ||
76 | } | ||
77 | |||
78 | return &Key{combined[:32], combined[32:64]} | ||
79 | } | ||
80 | |||
81 | func NewKeyPBKDF2(pass []byte, p *OPProfile) *Key { | ||
82 | return NewKey(pbkdf2.Key(pass, p.Salt, p.Iterations, 64, sha512.New)) | ||
83 | } | ||
84 | |||
85 | type OPBandItem struct { | ||
86 | Profile *OPProfile `json:"-"` | ||
87 | UUID string `json:"uuid"` | ||
88 | Category string `json:"category"` | ||
89 | CategoryName string `json:"-"` | ||
90 | Folder string `json:"folder"` | ||
91 | Trashed bool `json:"trashed"` | ||
92 | Favorite int `json:"fave"` | ||
93 | Data []byte `json:"d"` | ||
94 | HMAC []byte `json:"hmac"` | ||
95 | Key []byte `json:"k"` | ||
96 | Overview []byte `json:"o"` | ||
97 | Created time.Time `json:"created"` | ||
98 | TransactionTime time.Time `json:"tx"` | ||
99 | Updated time.Time `json:"updated"` | ||
100 | } | ||
101 | |||
102 | func (i *OPBandItem) UnmarshalJSON(data []byte) error { | ||
103 | var err error | ||
104 | type LocalItem OPBandItem | ||
105 | |||
106 | ti := &struct { | ||
107 | Data string `json:"d"` | ||
108 | HMAC string `json:"hmac"` | ||
109 | Key string `json:"k"` | ||
110 | Overview string `json:"o"` | ||
111 | Created int64 `json:"created"` | ||
112 | Updated int64 `json:"updated"` | ||
113 | TransactionTime int64 `json:"tx"` | ||
114 | *LocalItem | ||
115 | }{ | ||
116 | LocalItem: (*LocalItem)(i), | ||
117 | } | ||
118 | |||
119 | if err = json.Unmarshal(data, &ti); err != nil { | ||
120 | return err | ||
121 | } | ||
122 | |||
123 | i.Data, err = base64.StdEncoding.DecodeString(ti.Data) | ||
124 | if err != nil { | ||
125 | return err | ||
126 | } | ||
127 | |||
128 | i.HMAC, err = base64.StdEncoding.DecodeString(ti.HMAC) | ||
129 | if err != nil { | ||
130 | return err | ||
131 | } | ||
132 | |||
133 | i.Key, err = base64.StdEncoding.DecodeString(ti.Key) | ||
134 | if err != nil { | ||
135 | return err | ||
136 | } | ||
137 | |||
138 | i.Overview, err = base64.StdEncoding.DecodeString(ti.Overview) | ||
139 | if err != nil { | ||
140 | return err | ||
141 | } | ||
142 | |||
143 | i.CategoryName = CategoriesRev[i.Category] | ||
144 | i.Created = time.Unix(ti.Created, 0) | ||
145 | i.Updated = time.Unix(ti.Updated, 0) | ||
146 | i.TransactionTime = time.Unix(ti.TransactionTime, 0) | ||
147 | |||
148 | return nil | ||
149 | } | ||
150 | |||
151 | func (i *OPBandItem) DecryptKey(key *Key) (*Key, error) { | ||
152 | data := i.Key | ||
153 | |||
154 | mac := hmac.New(sha256.New, key.Mac) | ||
155 | mac.Write(data[:len(data)-32]) | ||
156 | expectedMac := mac.Sum(nil) | ||
157 | |||
158 | if !hmac.Equal(data[len(data)-32:len(data)], expectedMac) { | ||
159 | return nil, errors.New("HMAC does not match") | ||
160 | } | ||
161 | |||
162 | iv := data[:16] | ||
163 | ct := data[16 : len(data)-32] | ||
164 | |||
165 | block, err := aes.NewCipher(key.Encryption) | ||
166 | if err != nil { | ||
167 | return nil, err | ||
168 | } | ||
169 | |||
170 | if len(ct)%aes.BlockSize != 0 { | ||
171 | return nil, errors.New("Ciphertext is not multiple of block size") | ||
172 | } | ||
173 | |||
174 | mode := cipher.NewCBCDecrypter(block, iv) | ||
175 | mode.CryptBlocks(ct, ct) | ||
176 | |||
177 | return NewKey(ct), nil | ||
178 | } | ||
179 | |||
180 | func (i *OPBandItem) DecryptData() ([]byte, error) { | ||
181 | k, err := i.DecryptKey(i.Profile.masterKey) | ||
182 | if err != nil { | ||
183 | return nil, err | ||
184 | } | ||
185 | |||
186 | d, err := ParseOpdata01(i.Data, k) | ||
187 | if err != nil { | ||
188 | return nil, err | ||
189 | } | ||
190 | |||
191 | return d, nil | ||
192 | } | ||
193 | |||
194 | func (i *OPBandItem) DecryptOverview() ([]byte, error) { | ||
195 | o, err := ParseOpdata01(i.Overview, i.Profile.overviewKey) | ||
196 | if err != nil { | ||
197 | return nil, err | ||
198 | } | ||
199 | |||
200 | return o, nil | ||
201 | } | ||
202 | |||
203 | type OPProfile struct { | ||
204 | Path string `json:"-"` | ||
205 | UUID string `json:"uuid"` | ||
206 | ProfileName string `json:"profileName"` | ||
207 | Iterations int `json:"iterations"` | ||
208 | Salt []byte `json:"salt"` | ||
209 | MasterKey []byte `json:"masterKey"` | ||
210 | OverviewKey []byte `json:"overviewKey"` | ||
211 | CreatedAt time.Time `json:"createdAt"` | ||
212 | UpdatedAt time.Time `json:"updatedAt"` | ||
213 | LastUpdatedBy string `json:"lastUpdatedBy"` | ||
214 | |||
215 | masterKey *Key | ||
216 | overviewKey *Key | ||
217 | Items map[string]*OPBandItem `json:"-"` | ||
218 | } | ||
219 | |||
220 | func (p *OPProfile) UnmarshalJSON(data []byte) error { | ||
221 | var err error | ||
222 | type LocalProfile OPProfile | ||
223 | |||
224 | tp := &struct { | ||
225 | CreatedAt int64 `json:"createdAt"` | ||
226 | UpdatedAt int64 `json:"updatedAt"` | ||
227 | Salt string `json:"salt"` | ||
228 | MasterKey string `json:"masterKey"` | ||
229 | OverviewKey string `json:"overviewKey"` | ||
230 | *LocalProfile | ||
231 | }{ | ||
232 | LocalProfile: (*LocalProfile)(p), | ||
233 | } | ||
234 | |||
235 | if err = json.Unmarshal(data, &tp); err != nil { | ||
236 | return err | ||
237 | } | ||
238 | |||
239 | p.Salt, err = base64.StdEncoding.DecodeString(tp.Salt) | ||
240 | if err != nil { | ||
241 | return err | ||
242 | } | ||
243 | |||
244 | p.MasterKey, err = base64.StdEncoding.DecodeString(tp.MasterKey) | ||
245 | if err != nil { | ||
246 | return err | ||
247 | } | ||
248 | |||
249 | p.OverviewKey, err = base64.StdEncoding.DecodeString(tp.OverviewKey) | ||
250 | if err != nil { | ||
251 | return err | ||
252 | } | ||
253 | |||
254 | p.CreatedAt = time.Unix(tp.CreatedAt, 0) | ||
255 | p.UpdatedAt = time.Unix(tp.UpdatedAt, 0) | ||
256 | |||
257 | return nil | ||
258 | } | ||
259 | |||
260 | func (p *OPProfile) Unlock(masterpass []byte) error { | ||
261 | var err error | ||
262 | derived := NewKeyPBKDF2(masterpass, p) | ||
263 | |||
264 | p.masterKey, err = DeriveKey(p.MasterKey, derived) | ||
265 | if err != nil { | ||
266 | return err | ||
267 | } | ||
268 | |||
269 | p.overviewKey, err = DeriveKey(p.OverviewKey, derived) | ||
270 | if err != nil { | ||
271 | return err | ||
272 | } | ||
273 | |||
274 | return nil | ||
275 | } | ||
276 | |||
277 | func (p *OPProfile) LoadAllBands() { | ||
278 | for i := 0; i < 16; i++ { | ||
279 | _ = p.LoadBand(fmt.Sprintf("%X", i)) | ||
280 | } | ||
281 | } | ||
282 | |||
283 | func (p *OPProfile) LoadBand(band string) error { | ||
284 | c, err := ioutil.ReadFile(path.Join(p.Path, "default", fmt.Sprintf("band_%s.js", band))) | ||
285 | if err != nil { | ||
286 | return err | ||
287 | } | ||
288 | |||
289 | // JSON surrounded by "ld({...json...});" | ||
290 | b := make(map[string]*OPBandItem) | ||
291 | if err = json.Unmarshal(c[3:len(c)-2], &b); err != nil { | ||
292 | return err | ||
293 | } | ||
294 | |||
295 | for k, v := range b { | ||
296 | v.Profile = p | ||
297 | o, err := v.DecryptOverview() | ||
298 | if err == nil { | ||
299 | v.Overview = o | ||
300 | } | ||
301 | p.Items[k] = v | ||
302 | } | ||
303 | |||
304 | return nil | ||
305 | } | ||
306 | |||
307 | func LoadProfile(vaultpath string, masterpass []byte) (*OPProfile, error) { | ||
308 | p, err := NewProfile(vaultpath) | ||
309 | if err != nil { | ||
310 | return nil, err | ||
311 | } | ||
312 | |||
313 | err = p.Unlock(masterpass) | ||
314 | if err != nil { | ||
315 | return nil, err | ||
316 | } | ||
317 | |||
318 | // Wipe master password buffer | ||
319 | for i, _ := range masterpass { | ||
320 | masterpass[i] = 0 | ||
321 | } | ||
322 | |||
323 | p.LoadAllBands() | ||
324 | |||
325 | return p, nil | ||
326 | } | ||
327 | |||
328 | func NewProfile(vaultpath string) (*OPProfile, error) { | ||
329 | c, err := ioutil.ReadFile(path.Join(vaultpath, "default", "profile.js")) | ||
330 | if err != nil { | ||
331 | return nil, err | ||
332 | } | ||
333 | |||
334 | cut := bytes.Index(c, []byte("{")) | ||
335 | if cut == -1 { | ||
336 | return nil, errors.New("Profile not a valid JSON document") | ||
337 | } | ||
338 | |||
339 | // JSON surrounded by "var profile={...json...};" | ||
340 | p := &OPProfile{ | ||
341 | Path: vaultpath, | ||
342 | Items: make(map[string]*OPBandItem), | ||
343 | } | ||
344 | if err = json.Unmarshal(c[cut:len(c)-1], &p); err != nil { | ||
345 | return nil, err | ||
346 | } | ||
347 | |||
348 | return p, nil | ||
349 | } | ||
350 | |||
351 | func ParseOpdata01(data []byte, key *Key) ([]byte, error) { | ||
352 | // Validate data header | ||
353 | if !bytes.Equal(data[:8], []byte("opdata01")) { | ||
354 | return nil, errors.New("opdata01 header mismatch") | ||
355 | } | ||
356 | |||
357 | // Validate HMAC before we continue | ||
358 | mac := hmac.New(sha256.New, key.Mac) | ||
359 | mac.Write(data[:len(data)-32]) | ||
360 | expectedMac := mac.Sum(nil) | ||
361 | |||
362 | if !hmac.Equal(data[len(data)-32:len(data)], expectedMac) { | ||
363 | return nil, errors.New("HMAC does not match") | ||
364 | } | ||
365 | |||
366 | iv := data[16:32] | ||
367 | ct := data[32 : len(data)-32] | ||
368 | plaintextLen := binary.LittleEndian.Uint64(data[8:16]) | ||
369 | |||
370 | block, err := aes.NewCipher(key.Encryption) | ||
371 | if err != nil { | ||
372 | return nil, err | ||
373 | } | ||
374 | |||
375 | if len(ct)%aes.BlockSize != 0 { | ||
376 | return nil, errors.New("Ciphertext is not multiple of block size") | ||
377 | } | ||
378 | |||
379 | mode := cipher.NewCBCDecrypter(block, iv) | ||
380 | mode.CryptBlocks(ct, ct) | ||
381 | |||
382 | // Copy so we can free the buffer with IV/padding prefix | ||
383 | out := make([]byte, plaintextLen) | ||
384 | copy(out, ct[len(ct)-int(plaintextLen):len(ct)]) | ||
385 | |||
386 | return out, nil | ||
387 | } | ||
388 | |||
389 | func DeriveKey(data []byte, dkey *Key) (*Key, error) { | ||
390 | raw, err := ParseOpdata01(data, dkey) | ||
391 | if err != nil { | ||
392 | return nil, err | ||
393 | } | ||
394 | |||
395 | h := sha512.New() | ||
396 | h.Write(raw) | ||
397 | |||
398 | return NewKey(h.Sum(nil)), nil | ||
399 | } | ||
400 | |||
401 | func main() { | ||
402 | datapath := "~/Dropbox/1Password/1Password.opvault" | ||
403 | if strings.HasPrefix(datapath, "~/") { | ||
404 | u, _ := user.Current() | ||
405 | datapath = u.HomeDir + datapath[1:len(datapath)] | ||
406 | } | ||
407 | |||
408 | fmt.Print("Enter Password: ") | ||
409 | password, err := terminal.ReadPassword(int(syscall.Stdin)) | ||
410 | if err != nil { | ||
411 | fmt.Println("Unable to read password") | ||
412 | return | ||
413 | } | ||
414 | fmt.Println("") | ||
415 | |||
416 | p, err := LoadProfile(datapath, password) | ||
417 | if err != nil { | ||
418 | fmt.Println(err) | ||
419 | return | ||
420 | } | ||
421 | |||
422 | // Login - CD05161569D347ADB401DE06D30A0A89 | ||
423 | // Note - 7FF3565B434B47CF8906869BDCAD28C3 | ||
424 | // Password - 7059A882C5F84DDCBD2EAD9EFFAA2B58 | ||
425 | // Router - 6009533D5A3B483A93FC7A843C39EDED | ||
426 | // Server - 7BDDE92045834A5386D56576DAEDDE54 | ||
427 | // Credit - 6B9AFF656D264EEF8A887F79D243AE0D | ||
428 | // SW License - 7529DADA9453426BAF8B23A931834B2A | ||
429 | // Database - 102 | ||
430 | // Email - 111 | ||
431 | |||
432 | item, ok := p.Items[""] | ||
433 | if !ok { | ||
434 | fmt.Println("UUID not found in profile") | ||
435 | return | ||
436 | } | ||
437 | |||
438 | itemD, err := item.DecryptData() | ||
439 | if err != nil { | ||
440 | fmt.Println("Error decoding item data") | ||
441 | fmt.Println(err) | ||
442 | return | ||
443 | } | ||
444 | |||
445 | fmt.Printf("[\"%s\", %s, %s]", item.CategoryName, item.Overview, itemD) | ||
446 | } | ||