Администрирование
Работа с API
Импорт из Bitwarden
10min
Импорт паролей из файла Bitwarden в Пассворк через API. Запустите скрипт import.js и следуйте инструкциям.
💡 Важно
TOTP-коды должны быть валидные, иначе скрипт завершится с ошибкой
- Получить права root и обновить локальную базу данных пакетов:
Shell
1sudo -i
2apt-get update
- Установить Node.js и npm:
Shell
1apt install nodejs npm -y
Для успешного импорта версия Node.js должна быть выше 16.
- Проверка установленной версии:
Shell
1node -v
- Установка модулей для импорта:
Shell
1npm install dotenv readline fs util passwork-js
- Создать файл импорта — import.js
Исходный код import.js
Shell
1require("util").inspect.defaultOptions.depth = null;
2const env = require('dotenv').config().parsed;
3const readline = require('readline');
4const fs = require('fs');
5const Passwork = require('./node_modules/passwork-js/src/passwork-api');
6/** @type PassworkAPI */
7const passwork = new Passwork(env.HOST);
8
9function throwFatalError(message, error) {
10 console.error(message);
11 console.error(error);
12 process.exit(0);
13}
14
15(async () => {
16 try {
17 const [argFileName, argCollections, argPath] = process.argv.slice(2);
18 let jsonFileName;
19 let jsonData;
20 let collectionsToImport = [];
21 let importVault;
22
23 // Authorize
24 try {
25 await passwork.login(env.API_KEY, env.USER_MASTER_PASS);
26 } catch (e) {
27 throwFatalError('Не удалось авторизоваться', e);
28 }
29
30 const rl = readline.createInterface({
31 input: process.stdin,
32 output: process.stdout
33 });
34
35 // Read json from bitwarden
36 const answerFileName = await new Promise(resolve => {
37 rl.question('\nУкажите файл для экспорта\n', resolve)
38 });
39 jsonFileName = answerFileName ? answerFileName : argFileName;
40 try {
41 jsonData = JSON.parse(fs.readFileSync(jsonFileName));
42 if (!jsonData || !jsonData.hasOwnProperty('items')) {
43 throw 'Неверный формат json файл';
44 }
45 } catch (e) {
46 throwFatalError('Не удалось прочитать json файл', e);
47 }
48
49 // Specify collections to import
50 const answerCollections = await new Promise(resolve => {
51 rl.question('\nУкажите через запятую id или название коллекций для экспорта (необязательно)\n', resolve)
52 });
53 let collections = answerCollections ? answerCollections : argCollections;
54 if (collections) {
55 collections = collections.split(',').map(c => c.trim()).filter((c) => c);
56 } else {
57 collections = [];
58 }
59 if (jsonData.collections && jsonData.collections.length) {
60 if (collections.length === 0) {
61 collectionsToImport = jsonData.collections;
62 } else {
63 jsonData.collections.forEach(c => {
64 if (collections.includes(c.name) || collections.includes(c.id)) {
65 collectionsToImport.push(c);
66 }
67 });
68 }
69 } else {
70 collectionsToImport = [];
71 }
72 collectionsToImport = [...new Set(collectionsToImport)];
73
74 // Specify vault id for import
75 const answerPath = await new Promise(resolve => {
76 rl.question('\nУкажите id сейфа для импорта (необязательно) \n', resolve)
77 });
78 let path = answerPath ? answerPath : argPath;
79 if (path) {
80 importVault = await passwork.getVault(path);
81 if (!importVault) {
82 throwFatalError('Указанный для импорта сейф не найден');
83 }
84 }
85
86 // Confirm import
87 let confirmMessage = '\nБудут экспортированы следующие коллекции:\n';
88 if (jsonData.collections) {
89 collectionsToImport.forEach(c => {
90 confirmMessage += `${c.name} (${c.id})\n`;
91 });
92 } else {
93 confirmMessage += 'Личный сейф\n';
94 }
95 if (importVault) {
96 confirmMessage += `\nЭкспорт будет произведен в "${importVault.name}"\n`;
97 }
98 confirmMessage += 'Продолжить? y/n\n';
99
100 const answerConfirm = await new Promise(resolve => {
101 rl.question(confirmMessage, resolve)
102 });
103 if (answerConfirm.toLowerCase() === 'y') {
104 rl.close();
105 importPasswords().then(() => process.exit(0)).catch((e) => {
106 throwFatalError('error', e);
107 });
108 } else {
109 console.log('Операция отменена');
110 process.exit(0);
111 }
112
113 async function importPasswords() {
114 const logFileName = 'import-' + new Date().getTime() + '.log';
115
116 function logMessage(message) {
117 let msg = new Date().toISOString() + ' ' + message + '\n';
118 fs.appendFileSync(logFileName, msg);
119 console.log(msg);
120 }
121
122 function preparePasswordFields(data, directories) {
123 const vaultsNames = getDirectoriesNames(directories);
124
125 if (data.type !== 1 && data.type !== 2) {
126 logMessage(`Объект типа ${data.type}, ${data.name}`
127 + ` из коллекций ${vaultsNames} не был импортирован`);
128 return;
129 }
130 const fields = {
131 password: '',
132 name: data.name,
133 description: data.notes,
134 custom: [],
135 };
136 if (directories.length > 1) {
137 fields.description = fields.description ? (fields.description + '\n') : '';
138 fields.description += `Копия пароля находится в: ${vaultsNames}`;
139 }
140 if (data.login) {
141 if (data.login.username) {
142 fields.login = data.login.username;
143 }
144 if (data.login.password) {
145 fields.password = data.login.password;
146 }
147 if (data.login.totp) {
148 fields.custom.push({
149 name: 'TOTP',
150 value: data.login.totp,
151 type: 'totp'
152 });
153 }
154 if (data.login.uris) {
155 fields.url = data.login.uris.length === 1
156 ? data.login.uris[0].uri : data.login.uris.reduce((a, b) => (a.uri || a) + ", " + b.uri, '')
157 }
158 }
159 if (data.fields) {
160 data.fields.forEach((field) => {
161 if (field.type === 0 || field.type === 2) {
162 fields.custom.push({
163 name: String(field.name),
164 value: String(field.value),
165 type: 'text'
166 });
167 } else if (field.type === 1) {
168 fields.custom.push({
169 name: String(field.name),
170 value: String(field.value),
171 type: 'password'
172 });
173 } else {
174 logMessage(`Поле типа "link" объекта ${data.name}`
175 + ` из коллекций ${vaultsNames} не было импортирован`);
176 }
177 });
178 }
179
180 return fields;
181 }
182
183 function getDirectories(passwordCollectionIds, collections) {
184 const directories = [];
185 for (const collectionId of passwordCollectionIds) {
186 if (collections.hasOwnProperty(collectionId)) {
187 directories.push(collections[collectionId]);
188 }
189 }
190 return directories;
191 }
192
193 function getDirectoriesNames(directories) {
194 return directories.length > 1
195 ? directories.reduce((a, b) => (a.name || a) + ", " + b.name)
196 : directories[0].name;
197 }
198
199 logMessage('Импорт из файла ' + jsonFileName);
200
201 if (collectionsToImport.length) {
202 if (importVault) {
203 // Collections as folders
204 const folders = {};
205 for (let c = 0; c < collectionsToImport.length; c++) {
206 const item = collectionsToImport[c];
207 folders[item.id] = await passwork.addFolder(importVault.id, item.name);
208 logMessage(`Создана папка ${folders[item.id].name} на основе коллекции ${item.id}`)
209 }
210 for (let p = 0; p < jsonData.items.length; p++) {
211 const passwordData = jsonData.items[p];
212 const foldersList = getDirectories(passwordData.collectionIds, folders);
213 if (foldersList.length === 0) {
214 continue;
215 }
216
217 logMessage(`Начат импорт ${passwordData.name}`);
218 let fields = preparePasswordFields(passwordData, foldersList);
219 if (!fields) {
220 continue;
221 }
222 fields.vaultId = importVault.id;
223 for (const folder of foldersList) {
224 fields.folderId = folder.id;
225 await passwork.addPassword(Object.assign({}, fields));
226 logMessage(`Завершен импорт ${passwordData.name}`);
227 }
228 }
229 } else {
230 // Collections as vaults
231 const vaults = [];
232 for (let c = 0; c < collectionsToImport.length; c++) {
233 const item = collectionsToImport[c];
234 const vaultId = await passwork.addVault(item.name);
235 vaults[item.id] = await passwork.getVault(vaultId);
236 logMessage(`Создан сейф ${vaults[item.id].name} на основе коллекции ${item.id}`)
237 }
238 for (let p = 0; p < jsonData.items.length; p++) {
239 const passwordData = jsonData.items[p];
240 const vaultsList = getDirectories(passwordData.collectionIds, vaults);
241 if (vaultsList.length === 0) {
242 continue;
243 }
244
245 logMessage(`Начат импорт ${passwordData.name}`);
246 let fields = preparePasswordFields(passwordData, vaultsList);
247 if (!fields) {
248 continue;
249 }
250 for (const vault of vaultsList) {
251 fields.vaultId = vault.id;
252 await passwork.addPassword(Object.assign({}, fields));
253 logMessage(`Завершен импорт ${passwordData.name}`);
254 }
255 }
256 }
257 logMessage(`Импорт завершен`);
258 process.exit(0);
259 return;
260 }
261
262 if (collectionsToImport.length === 0 && jsonData.items[0].organizationId === null) {
263 // Private vault import
264 if (!importVault) {
265 const vaultId = await passwork.addVault('Личный сейф', true);
266 importVault = await passwork.getVault(vaultId);
267 logMessage(`Сейф ${importVault.name} был создан `);
268 }
269 const folders = {};
270 if (jsonData.folders) {
271 for (const folder of jsonData.folders) {
272 folders[folder.id] = await passwork.addFolder(importVault.id, folder.name);
273 }
274 }
275
276 for (let p = 0; p < jsonData.items.length; p++) {
277 const passwordData = jsonData.items[p];
278 logMessage(`Начат импорт ${passwordData.name}`);
279
280 let fields = preparePasswordFields(passwordData, [importVault]);
281 if (!fields) {
282 continue;
283 }
284 fields.vaultId = importVault.id;
285 if (passwordData.folderId) {
286 fields.folderId = folders[passwordData.folderId].id;
287 }
288 await passwork.addPassword(Object.assign({}, fields));
289 logMessage(`Импорт завершен ${passwordData.name}`);
290 }
291 logMessage(`Импорт завершен`);
292 process.exit(0);
293 return;
294 }
295
296 logMessage(`Не удалось определить формат импорта`);
297 process.exit(0);
298 }
299 } catch (e) {
300 throwFatalError('error', e);
301 }
302})();
- Создать файл .env и указать хост Пассворка, API ключ пользователя и его мастер пароль:
Shell
1HOST='https://your_host/api/v4'
2API_KEY=
3USER_MASTER_PASS=
- Загрузить XML файл Bitwarden и запустить скрипт. Скрипт запросит название файла:
Shell
1node import.js
- Также эти параметры можно передать аргументами в скрипт:
Shell
1node import.js bitwarden_export_org.json "Collection 1"
Обновлено 02 Oct 2024
Помогла ли вам эта страница?