Coverage for pass_import/managers/onepassword.py: 95%
98 statements
« prev ^ index » next coverage.py v7.4.3, created at 2024-02-26 12:11 +0000
« prev ^ index » next coverage.py v7.4.3, created at 2024-02-26 12:11 +0000
1# -*- encoding: utf-8 -*-
2# pass import - Passwords importer swiss army knife
3# Copyright (C) 2017-2024 Alexandre PUJOL <alexandre@pujol.io>.
4#
6import json
7import re
8from pass_import.core import register_managers, register_detecters
9from pass_import.formats.csv import CSV
10from pass_import.formats.json import JSON
13class OnePasswordCSV(CSV):
14 """Importer for 1password 6 in CSV format."""
15 name = '1password'
16 default = False
17 version = '6'
18 default = False
19 url = 'https://1password.com'
20 hexport = 'See this guide: https://support.1password.com/export'
21 himport = 'pass import 1password file.csv'
22 keys = {
23 'title': 'Title',
24 'password': 'Password',
25 'login': 'Username',
26 'url': 'URL',
27 'comments': 'Notes',
28 'group': 'Type'
29 }
32class OnePassword4CSV(CSV):
33 """Importer for 1password 4 in CSV format."""
34 name = '1password'
35 default = False
36 version = '4'
37 only = True
38 url = 'https://1password.com'
39 hexport = 'See this guide: https://support.1password.com/export'
40 himport = 'pass import 1password file.csv'
41 keys = {
42 'title': 'title',
43 'password': 'password',
44 'login': 'username',
45 'url': 'url',
46 'comments': 'notes'
47 }
50class OnePassword8CSV(CSV):
51 """Importer for 1password 8 in CSV format."""
52 name = '1password'
53 version = '8'
54 url = 'https://1password.com'
55 hexport = 'See this guide: https://support.1password.com/export'
56 himport = 'pass import 1password file.csv'
57 keys = {
58 'title': 'Title',
59 'url': 'Url',
60 'login': 'Username',
61 'password': 'Password',
62 'otpauth': 'OTPAuth',
63 'favorite': 'Favorite',
64 'archived': 'Archived',
65 'tags': 'Tags',
66 'comments': 'Notes'
67 }
70class OnePassword4PIF(JSON):
71 """Importer for 1password 4 in PIF format.
73 :param list ignore: List of key in the PIF file to not try to import.
75 """
76 name = '1password'
77 format = '1pif'
78 default = False
79 version = '4'
80 url = 'https://1password.com'
81 hexport = 'See this guide: https://support.1password.com/export'
82 himport = 'pass import 1password file.1pif'
83 encoding = 'utf-8-sig'
84 ignore = {'keyID', 'typeName', 'uuid', 'openContents', 'URLs'}
85 keys = {
86 'title': 'title',
87 'password': 'password',
88 'login': 'username',
89 'url': 'location',
90 'comments': 'notesPlain',
91 'group': 'folderUuid',
92 'tags': 'tags'
93 }
95 # Import methods
97 @staticmethod
98 def pif2json(file):
99 """Convert 1pif to json: https://github.com/eblin/1passpwnedcheck."""
100 data = file.read()
101 cleaned = re.sub(r'(?m)^\*\*\*.*\*\*\*\s+', '', data)
102 cleaned = cleaned.split('\n')
103 # On 1Password v7.9.11 (macOS), 1PIF export produces 1 extra empty line
104 cleaned = [v for v in cleaned if len(v) > 0]
105 cleaned = ','.join(cleaned).rstrip(',')
106 cleaned = f'[{cleaned}]'
107 # JSON string with eventual special characters are encoded properly
108 # eg: NUL, TAB
109 cleaned = json.dumps(json.loads(cleaned, strict=False))
110 return json.loads(cleaned)
112 def parse(self):
113 """Parse PIF based file."""
114 jsons = self.pif2json(self.file)
115 keys = self.invkeys()
116 folders = {}
117 for item in jsons:
118 if item.get('typeName', '') == 'system.folder.Regular':
119 key = item.get('uuid', '')
120 folders[key] = {
121 'group': item.get('title', ''),
122 'parent': item.get('folderUuid', '')
123 }
125 elif item.get('typeName', '') == 'webforms.WebForm':
126 if item.get('trashed', False):
127 continue
128 entry = {}
129 scontent = item.pop('secureContents', {})
130 fields = scontent.pop('fields', [])
131 for field in fields:
132 name = field.get('name', '')
133 designation = field.get('designation', '')
134 jsonkey = name or designation
135 key = keys.get(jsonkey, jsonkey)
136 entry[key] = field.get('value', '')
138 sections = scontent.get('sections', [])
139 for section in sections:
140 for field in section.get('fields', []):
141 value = field.get('v', '')
142 if value.startswith('otpauth://'):
143 entry['otpauth'] = value
145 item.update(scontent)
146 for key, value in item.items():
147 if key not in self.ignore:
148 entry[keys.get(key, key)] = value
150 tags = []
151 if 'openContents' in item:
152 open_contents = item['openContents']
153 tags = open_contents.get('tags', [])
155 entry['tags'] = tags
156 self.data.append(entry)
157 self._sortgroup(folders)
159 # Format recognition method
161 def is_format(self):
162 """Return True if the file is a 1PIF file."""
163 try:
164 self.jsons = self.pif2json(self.file)
165 except (json.decoder.JSONDecodeError, UnicodeDecodeError):
166 return False
167 return True
169 def checkheader(self, header, only=False):
170 """No header check is needed."""
171 return True
174register_managers(OnePassword8CSV,
175 OnePasswordCSV, OnePassword4CSV, OnePassword4PIF)
176register_detecters(OnePassword4PIF)