From b96e751c8944c1b63bf78033c76892c72abad432 Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Sat, 26 Nov 2016 14:43:59 -0800 Subject: Initial import --- main.go | 446 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 446 insertions(+) create mode 100644 main.go diff --git a/main.go b/main.go new file mode 100644 index 0000000..88cca34 --- /dev/null +++ b/main.go @@ -0,0 +1,446 @@ +package main + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "os/user" + "path" + "strings" + "syscall" + "time" + + "golang.org/x/crypto/pbkdf2" + "golang.org/x/crypto/ssh/terminal" +) + +var Categories = map[string]string{ + "Login": "001", + "Credit Card": "002", + "Secure Note": "003", + "Identity": "004", + "Password": "005", + "Tombstone": "099", + "Software License": "100", + "Bank Account": "101", + "Database": "102", + "Driver License": "103", + "Outdoor License": "104", + "Membership": "105", + "Passport": "106", + "Rewards": "107", + "SSN": "108", + "Router": "109", + "Server": "110", + "Email": "111", +} + +var CategoriesRev = map[string]string{ + "001": "Login", + "002": "Credit Card", + "003": "Secure Note", + "004": "Identity", + "005": "Password", + "099": "Tombstone", + "100": "Software License", + "101": "Bank Account", + "102": "Database", + "103": "Driver License", + "104": "Outdoor License", + "105": "Membership", + "106": "Passport", + "107": "Rewards", + "108": "SSN", + "109": "Router", + "110": "Server", + "111": "Email", +} + +type Key struct { + Encryption []byte + Mac []byte +} + +func NewKey(combined []byte) *Key { + if len(combined) != 64 { + panic("Invalid key size") + } + + return &Key{combined[:32], combined[32:64]} +} + +func NewKeyPBKDF2(pass []byte, p *OPProfile) *Key { + return NewKey(pbkdf2.Key(pass, p.Salt, p.Iterations, 64, sha512.New)) +} + +type OPBandItem struct { + Profile *OPProfile `json:"-"` + UUID string `json:"uuid"` + Category string `json:"category"` + CategoryName string `json:"-"` + Folder string `json:"folder"` + Trashed bool `json:"trashed"` + Favorite int `json:"fave"` + Data []byte `json:"d"` + HMAC []byte `json:"hmac"` + Key []byte `json:"k"` + Overview []byte `json:"o"` + Created time.Time `json:"created"` + TransactionTime time.Time `json:"tx"` + Updated time.Time `json:"updated"` +} + +func (i *OPBandItem) UnmarshalJSON(data []byte) error { + var err error + type LocalItem OPBandItem + + ti := &struct { + Data string `json:"d"` + HMAC string `json:"hmac"` + Key string `json:"k"` + Overview string `json:"o"` + Created int64 `json:"created"` + Updated int64 `json:"updated"` + TransactionTime int64 `json:"tx"` + *LocalItem + }{ + LocalItem: (*LocalItem)(i), + } + + if err = json.Unmarshal(data, &ti); err != nil { + return err + } + + i.Data, err = base64.StdEncoding.DecodeString(ti.Data) + if err != nil { + return err + } + + i.HMAC, err = base64.StdEncoding.DecodeString(ti.HMAC) + if err != nil { + return err + } + + i.Key, err = base64.StdEncoding.DecodeString(ti.Key) + if err != nil { + return err + } + + i.Overview, err = base64.StdEncoding.DecodeString(ti.Overview) + if err != nil { + return err + } + + i.CategoryName = CategoriesRev[i.Category] + i.Created = time.Unix(ti.Created, 0) + i.Updated = time.Unix(ti.Updated, 0) + i.TransactionTime = time.Unix(ti.TransactionTime, 0) + + return nil +} + +func (i *OPBandItem) DecryptKey(key *Key) (*Key, error) { + data := i.Key + + mac := hmac.New(sha256.New, key.Mac) + mac.Write(data[:len(data)-32]) + expectedMac := mac.Sum(nil) + + if !hmac.Equal(data[len(data)-32:len(data)], expectedMac) { + return nil, errors.New("HMAC does not match") + } + + iv := data[:16] + ct := data[16 : len(data)-32] + + block, err := aes.NewCipher(key.Encryption) + if err != nil { + return nil, err + } + + if len(ct)%aes.BlockSize != 0 { + return nil, errors.New("Ciphertext is not multiple of block size") + } + + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(ct, ct) + + return NewKey(ct), nil +} + +func (i *OPBandItem) DecryptData() ([]byte, error) { + k, err := i.DecryptKey(i.Profile.masterKey) + if err != nil { + return nil, err + } + + d, err := ParseOpdata01(i.Data, k) + if err != nil { + return nil, err + } + + return d, nil +} + +func (i *OPBandItem) DecryptOverview() ([]byte, error) { + o, err := ParseOpdata01(i.Overview, i.Profile.overviewKey) + if err != nil { + return nil, err + } + + return o, nil +} + +type OPProfile struct { + Path string `json:"-"` + UUID string `json:"uuid"` + ProfileName string `json:"profileName"` + Iterations int `json:"iterations"` + Salt []byte `json:"salt"` + MasterKey []byte `json:"masterKey"` + OverviewKey []byte `json:"overviewKey"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + LastUpdatedBy string `json:"lastUpdatedBy"` + + masterKey *Key + overviewKey *Key + Items map[string]*OPBandItem `json:"-"` +} + +func (p *OPProfile) UnmarshalJSON(data []byte) error { + var err error + type LocalProfile OPProfile + + tp := &struct { + CreatedAt int64 `json:"createdAt"` + UpdatedAt int64 `json:"updatedAt"` + Salt string `json:"salt"` + MasterKey string `json:"masterKey"` + OverviewKey string `json:"overviewKey"` + *LocalProfile + }{ + LocalProfile: (*LocalProfile)(p), + } + + if err = json.Unmarshal(data, &tp); err != nil { + return err + } + + p.Salt, err = base64.StdEncoding.DecodeString(tp.Salt) + if err != nil { + return err + } + + p.MasterKey, err = base64.StdEncoding.DecodeString(tp.MasterKey) + if err != nil { + return err + } + + p.OverviewKey, err = base64.StdEncoding.DecodeString(tp.OverviewKey) + if err != nil { + return err + } + + p.CreatedAt = time.Unix(tp.CreatedAt, 0) + p.UpdatedAt = time.Unix(tp.UpdatedAt, 0) + + return nil +} + +func (p *OPProfile) Unlock(masterpass []byte) error { + var err error + derived := NewKeyPBKDF2(masterpass, p) + + p.masterKey, err = DeriveKey(p.MasterKey, derived) + if err != nil { + return err + } + + p.overviewKey, err = DeriveKey(p.OverviewKey, derived) + if err != nil { + return err + } + + return nil +} + +func (p *OPProfile) LoadAllBands() { + for i := 0; i < 16; i++ { + _ = p.LoadBand(fmt.Sprintf("%X", i)) + } +} + +func (p *OPProfile) LoadBand(band string) error { + c, err := ioutil.ReadFile(path.Join(p.Path, "default", fmt.Sprintf("band_%s.js", band))) + if err != nil { + return err + } + + // JSON surrounded by "ld({...json...});" + b := make(map[string]*OPBandItem) + if err = json.Unmarshal(c[3:len(c)-2], &b); err != nil { + return err + } + + for k, v := range b { + v.Profile = p + o, err := v.DecryptOverview() + if err == nil { + v.Overview = o + } + p.Items[k] = v + } + + return nil +} + +func LoadProfile(vaultpath string, masterpass []byte) (*OPProfile, error) { + p, err := NewProfile(vaultpath) + if err != nil { + return nil, err + } + + err = p.Unlock(masterpass) + if err != nil { + return nil, err + } + + // Wipe master password buffer + for i, _ := range masterpass { + masterpass[i] = 0 + } + + p.LoadAllBands() + + return p, nil +} + +func NewProfile(vaultpath string) (*OPProfile, error) { + c, err := ioutil.ReadFile(path.Join(vaultpath, "default", "profile.js")) + if err != nil { + return nil, err + } + + cut := bytes.Index(c, []byte("{")) + if cut == -1 { + return nil, errors.New("Profile not a valid JSON document") + } + + // JSON surrounded by "var profile={...json...};" + p := &OPProfile{ + Path: vaultpath, + Items: make(map[string]*OPBandItem), + } + if err = json.Unmarshal(c[cut:len(c)-1], &p); err != nil { + return nil, err + } + + return p, nil +} + +func ParseOpdata01(data []byte, key *Key) ([]byte, error) { + // Validate data header + if !bytes.Equal(data[:8], []byte("opdata01")) { + return nil, errors.New("opdata01 header mismatch") + } + + // Validate HMAC before we continue + mac := hmac.New(sha256.New, key.Mac) + mac.Write(data[:len(data)-32]) + expectedMac := mac.Sum(nil) + + if !hmac.Equal(data[len(data)-32:len(data)], expectedMac) { + return nil, errors.New("HMAC does not match") + } + + iv := data[16:32] + ct := data[32 : len(data)-32] + plaintextLen := binary.LittleEndian.Uint64(data[8:16]) + + block, err := aes.NewCipher(key.Encryption) + if err != nil { + return nil, err + } + + if len(ct)%aes.BlockSize != 0 { + return nil, errors.New("Ciphertext is not multiple of block size") + } + + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(ct, ct) + + // Copy so we can free the buffer with IV/padding prefix + out := make([]byte, plaintextLen) + copy(out, ct[len(ct)-int(plaintextLen):len(ct)]) + + return out, nil +} + +func DeriveKey(data []byte, dkey *Key) (*Key, error) { + raw, err := ParseOpdata01(data, dkey) + if err != nil { + return nil, err + } + + h := sha512.New() + h.Write(raw) + + return NewKey(h.Sum(nil)), nil +} + +func main() { + datapath := "~/Dropbox/1Password/1Password.opvault" + if strings.HasPrefix(datapath, "~/") { + u, _ := user.Current() + datapath = u.HomeDir + datapath[1:len(datapath)] + } + + fmt.Print("Enter Password: ") + password, err := terminal.ReadPassword(int(syscall.Stdin)) + if err != nil { + fmt.Println("Unable to read password") + return + } + fmt.Println("") + + p, err := LoadProfile(datapath, password) + if err != nil { + fmt.Println(err) + return + } + + // Login - CD05161569D347ADB401DE06D30A0A89 + // Note - 7FF3565B434B47CF8906869BDCAD28C3 + // Password - 7059A882C5F84DDCBD2EAD9EFFAA2B58 + // Router - 6009533D5A3B483A93FC7A843C39EDED + // Server - 7BDDE92045834A5386D56576DAEDDE54 + // Credit - 6B9AFF656D264EEF8A887F79D243AE0D + // SW License - 7529DADA9453426BAF8B23A931834B2A + // Database - 102 + // Email - 111 + + item, ok := p.Items[""] + if !ok { + fmt.Println("UUID not found in profile") + return + } + + itemD, err := item.DecryptData() + if err != nil { + fmt.Println("Error decoding item data") + fmt.Println(err) + return + } + + fmt.Printf("[\"%s\", %s, %s]", item.CategoryName, item.Overview, itemD) +} -- cgit v1.2.3