zotero-plugin 5.0.27 → 5.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/fetch-zotero-log.py +85 -41
- package/bin/release.js +1 -1
- package/debug-log.d.ts +10 -0
- package/debug-log.js +63 -37
- package/package.json +1 -1
package/bin/fetch-zotero-log.py
CHANGED
|
@@ -4,57 +4,101 @@ import sys, os
|
|
|
4
4
|
import urllib.request
|
|
5
5
|
from zipfile import ZipFile
|
|
6
6
|
from types import SimpleNamespace
|
|
7
|
+
from pathlib import Path
|
|
7
8
|
|
|
8
|
-
from
|
|
9
|
-
from
|
|
10
|
-
|
|
9
|
+
from Cryptodome.PublicKey import RSA
|
|
10
|
+
from Cryptodome.Cipher import PKCS1_OAEP
|
|
11
|
+
|
|
12
|
+
def oops(*args):
|
|
13
|
+
print(*args)
|
|
14
|
+
sys.exit(1)
|
|
11
15
|
|
|
12
16
|
def debuglog():
|
|
13
|
-
|
|
17
|
+
if len(sys.argv) < 2:
|
|
18
|
+
oops('No log ID')
|
|
19
|
+
|
|
20
|
+
logid = Path(sys.argv[1])
|
|
21
|
+
tags = logid.suffixes
|
|
22
|
+
logid = str(logid).rstrip(''.join(tags))
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
key, host, remote = logid.split('-')
|
|
26
|
+
except ValueError:
|
|
27
|
+
oops('Invalid log ID', sys.argv[1])
|
|
28
|
+
|
|
14
29
|
if host != '0x0':
|
|
15
|
-
|
|
16
|
-
sys.exit(1)
|
|
30
|
+
oops('unexpected debug log host', host)
|
|
17
31
|
|
|
18
|
-
encrypted = ('.'
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
32
|
+
encrypted = ('.enc' in tags)
|
|
33
|
+
references = ('.refs' in tags)
|
|
34
|
+
cypher_rsa = None
|
|
35
|
+
if encrypted:
|
|
36
|
+
if len(len(sys.argv) != 3):
|
|
37
|
+
oops('no private key provided')
|
|
38
|
+
if Path(sys.argv[2]).suffix != '.pem':
|
|
39
|
+
oops('private key must be a .pem')
|
|
40
|
+
|
|
41
|
+
url = f'https://0x0.com/{remote}.zip'
|
|
42
|
+
SimpleNamespace(key=key, host=host, remote=remote, encrypted=encrypted, references=references, url=url),
|
|
43
|
+
debuglog = debuglog()
|
|
23
44
|
|
|
24
45
|
def download():
|
|
25
|
-
debuglog.
|
|
26
|
-
print(debuglog.url, '=>', debuglog.
|
|
46
|
+
debuglog.zip = f'logs/{debuglog.key}.zip'
|
|
47
|
+
print(debuglog.url, '=>', debuglog.zip)
|
|
27
48
|
logs = os.path.dirname(log)
|
|
28
49
|
if not os.path.exists(logs):
|
|
29
50
|
os.makedirs(logs)
|
|
30
|
-
urllib.request.urlretrieve(debuglog.url, debuglog.
|
|
31
|
-
|
|
32
|
-
def decrypt():
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
encrypted
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
urllib.request.urlretrieve(debuglog.url, debuglog.zip)
|
|
52
|
+
|
|
53
|
+
def decrypt(encrypted, iv, target):
|
|
54
|
+
encrypted = encrypted.read()
|
|
55
|
+
iv = iv.read()
|
|
56
|
+
|
|
57
|
+
# The last 16 bytes of the encrypted file are the authentication tag for GCM
|
|
58
|
+
tag = encrypted[-16:]
|
|
59
|
+
ciphertext = encrypted[:-16]
|
|
60
|
+
|
|
61
|
+
cipher_aes = AES.new(symmetric_key, AES.MODE_GCM, nonce=iv)
|
|
62
|
+
decrypted = cipher_aes.decrypt_and_verify(ciphertext, tag)
|
|
63
|
+
with open(target, 'wb') as f:
|
|
64
|
+
f.write(decrypted)
|
|
65
|
+
|
|
66
|
+
def unpack():
|
|
67
|
+
symmetric_key = None
|
|
68
|
+
|
|
69
|
+
with ZipFile(debuglog.zip) as zip:
|
|
70
|
+
files = zip.namelist()
|
|
71
|
+
|
|
72
|
+
symmetric_key = [f for f in files if Path(f).suffix == '.key']
|
|
73
|
+
match len(private_key):
|
|
74
|
+
case 0:
|
|
75
|
+
if debuglog.encrypted:
|
|
76
|
+
oops('No key found in', debuglog.key)
|
|
77
|
+
|
|
78
|
+
case 1:
|
|
79
|
+
with open(sys.argv[2], 'rb') as f:
|
|
80
|
+
private_key = RSA.import_key(f.read())
|
|
81
|
+
cipher_rsa = PKCS1_OAEP.new(private_key)
|
|
82
|
+
with zip.open(symmetric_key[0]) as f:
|
|
83
|
+
symmetric_key = cipher_rsa.decrypt(f.read())
|
|
84
|
+
|
|
85
|
+
case _:
|
|
86
|
+
oops('Multiple keys found in', debuglog.key)
|
|
87
|
+
|
|
88
|
+
for name in files:
|
|
89
|
+
match Path(name).suffix:
|
|
90
|
+
case '.key' | '.iv'
|
|
91
|
+
pass
|
|
92
|
+
case '.enc'
|
|
93
|
+
print(Path('logs') / name.with_suffix('.zip'), '(encrypted)')
|
|
94
|
+
iv = next((f for f in filename_list if f == Path(name).with_suffix('.iv')), None)
|
|
95
|
+
|
|
96
|
+
with zip.open(name) as f_data, open(iv) as f_iv:
|
|
97
|
+
decrypt(f_data, f_iv, str(Path('logs') / name))
|
|
98
|
+
|
|
99
|
+
case _:
|
|
100
|
+
print(Path('logs') / name)
|
|
101
|
+
f.extract(name, path='logs')
|
|
56
102
|
|
|
57
|
-
debuglog = debuglog()
|
|
58
103
|
download()
|
|
59
104
|
decrypt()
|
|
60
|
-
show()
|
package/bin/release.js
CHANGED
|
@@ -8448,7 +8448,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
8448
8448
|
"package.json"(exports, module) {
|
|
8449
8449
|
module.exports = {
|
|
8450
8450
|
name: "zotero-plugin",
|
|
8451
|
-
version: "5.0.
|
|
8451
|
+
version: "5.0.29",
|
|
8452
8452
|
description: "Zotero plugin builder",
|
|
8453
8453
|
homepage: "https://github.com/retorquere/zotero-plugin/wiki",
|
|
8454
8454
|
bin: {
|
package/debug-log.d.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
export declare class Bundler {
|
|
2
|
+
#private;
|
|
3
|
+
key: string;
|
|
4
|
+
private IV_LENGTH;
|
|
5
|
+
constructor(pubkey: string);
|
|
6
|
+
add(path: string, data: string, refs?: boolean): Promise<void>;
|
|
7
|
+
get zip(): ArrayBuffer;
|
|
8
|
+
get name(): string;
|
|
9
|
+
id(remote: string): string;
|
|
10
|
+
}
|
|
1
11
|
declare class DebugLogSender {
|
|
2
12
|
id: {
|
|
3
13
|
menu: string;
|
package/debug-log.js
CHANGED
|
@@ -1,9 +1,63 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/* eslint-disable no-magic-numbers */
|
|
3
|
+
var _Bundler_refs, _Bundler_symmetric, _Bundler_pubkey, _Bundler_crypto, _Bundler_subtle, _Bundler_files, _Bundler_encoder;
|
|
3
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.DebugLog = void 0;
|
|
5
|
+
exports.DebugLog = exports.Bundler = void 0;
|
|
5
6
|
const tslib_1 = require("tslib");
|
|
7
|
+
Components.utils.importGlobalProperties(['FormData']);
|
|
8
|
+
const pkg = require('./package.json');
|
|
6
9
|
const UZip = tslib_1.__importStar(require("uzip"));
|
|
10
|
+
class Bundler {
|
|
11
|
+
constructor(pubkey) {
|
|
12
|
+
_Bundler_refs.set(this, false);
|
|
13
|
+
this.IV_LENGTH = 12;
|
|
14
|
+
_Bundler_symmetric.set(this, void 0);
|
|
15
|
+
_Bundler_pubkey.set(this, void 0);
|
|
16
|
+
_Bundler_crypto.set(this, void 0);
|
|
17
|
+
_Bundler_subtle.set(this, void 0);
|
|
18
|
+
_Bundler_files.set(this, {});
|
|
19
|
+
_Bundler_encoder.set(this, new TextEncoder());
|
|
20
|
+
this.key = Zotero.Utilities.generateObjectKey();
|
|
21
|
+
tslib_1.__classPrivateFieldSet(this, _Bundler_pubkey, pubkey, "f");
|
|
22
|
+
tslib_1.__classPrivateFieldSet(this, _Bundler_crypto, Zotero.getMainWindow().crypto, "f");
|
|
23
|
+
tslib_1.__classPrivateFieldSet(this, _Bundler_subtle, tslib_1.__classPrivateFieldGet(this, _Bundler_crypto, "f").subtle, "f");
|
|
24
|
+
}
|
|
25
|
+
async add(path, data, refs = false) {
|
|
26
|
+
tslib_1.__classPrivateFieldSet(this, _Bundler_refs, tslib_1.__classPrivateFieldGet(this, _Bundler_refs, "f") || refs, "f");
|
|
27
|
+
const encoded = tslib_1.__classPrivateFieldGet(this, _Bundler_encoder, "f").encode(data);
|
|
28
|
+
if (tslib_1.__classPrivateFieldGet(this, _Bundler_pubkey, "f")) {
|
|
29
|
+
if (!tslib_1.__classPrivateFieldGet(this, _Bundler_symmetric, "f")) {
|
|
30
|
+
tslib_1.__classPrivateFieldSet(this, _Bundler_symmetric, await tslib_1.__classPrivateFieldGet(this, _Bundler_subtle, "f").generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']), "f");
|
|
31
|
+
const base64Pem = tslib_1.__classPrivateFieldGet(this, _Bundler_pubkey, "f")
|
|
32
|
+
.replace('-----BEGIN PUBLIC KEY-----', '')
|
|
33
|
+
.replace('-----END PUBLIC KEY-----', '')
|
|
34
|
+
.replace(/\s/g, '');
|
|
35
|
+
const keyBuffer = Uint8Array.from(atob(base64Pem), c => c.charCodeAt(0)).buffer;
|
|
36
|
+
const publicKey = await tslib_1.__classPrivateFieldGet(this, _Bundler_subtle, "f").importKey('spki', keyBuffer, { name: 'RSA-OAEP', hash: 'SHA-256' }, true, ['encrypt']);
|
|
37
|
+
const exportedKey = await tslib_1.__classPrivateFieldGet(this, _Bundler_subtle, "f").exportKey('raw', tslib_1.__classPrivateFieldGet(this, _Bundler_symmetric, "f"));
|
|
38
|
+
tslib_1.__classPrivateFieldGet(this, _Bundler_files, "f")[`${this.key}/${this.key}.key`] = new Uint8Array(await tslib_1.__classPrivateFieldGet(this, _Bundler_subtle, "f").encrypt({ name: 'RSA-OAEP' }, publicKey, exportedKey));
|
|
39
|
+
}
|
|
40
|
+
const iv = tslib_1.__classPrivateFieldGet(this, _Bundler_crypto, "f").getRandomValues(new Uint8Array(this.IV_LENGTH));
|
|
41
|
+
const encryptedData = await tslib_1.__classPrivateFieldGet(this, _Bundler_subtle, "f").encrypt({ name: 'AES-GCM', iv: iv }, tslib_1.__classPrivateFieldGet(this, _Bundler_symmetric, "f"), encoded);
|
|
42
|
+
tslib_1.__classPrivateFieldGet(this, _Bundler_files, "f")[`${this.key}/${path}.iv`] = iv;
|
|
43
|
+
tslib_1.__classPrivateFieldGet(this, _Bundler_files, "f")[`${this.key}/${path}.enc`] = new Uint8Array(encryptedData);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
tslib_1.__classPrivateFieldGet(this, _Bundler_files, "f")[`${this.key}/${path}`] = encoded;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
get zip() {
|
|
50
|
+
return UZip.encode(tslib_1.__classPrivateFieldGet(this, _Bundler_files, "f"));
|
|
51
|
+
}
|
|
52
|
+
get name() {
|
|
53
|
+
return `${this.key}.zip`;
|
|
54
|
+
}
|
|
55
|
+
id(remote) {
|
|
56
|
+
return `${this.key}-${remote}${tslib_1.__classPrivateFieldGet(this, _Bundler_refs, "f") ? '.refs' : ''}${tslib_1.__classPrivateFieldGet(this, _Bundler_pubkey, "f") ? '.enc' : ''}`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.Bundler = Bundler;
|
|
60
|
+
_Bundler_refs = new WeakMap(), _Bundler_symmetric = new WeakMap(), _Bundler_pubkey = new WeakMap(), _Bundler_crypto = new WeakMap(), _Bundler_subtle = new WeakMap(), _Bundler_files = new WeakMap(), _Bundler_encoder = new WeakMap();
|
|
7
61
|
class DebugLogSender {
|
|
8
62
|
constructor() {
|
|
9
63
|
this.id = {
|
|
@@ -67,63 +121,35 @@ class DebugLogSender {
|
|
|
67
121
|
Services.prompt.alert(null, 'Debug log submission error', `${err}`); // eslint-disable-line @typescript-eslint/restrict-template-expressions
|
|
68
122
|
});
|
|
69
123
|
}
|
|
70
|
-
async sendAsync(plugin, preferences, pubkey) {
|
|
124
|
+
async sendAsync(plugin, preferences, pubkey = '') {
|
|
71
125
|
await Zotero.Schema.schemaUpdatePromise;
|
|
72
|
-
const
|
|
73
|
-
const enc = new TextEncoder();
|
|
74
|
-
const key = Zotero.Utilities.generateObjectKey();
|
|
126
|
+
const bundler = new Bundler(pubkey);
|
|
75
127
|
let log = [
|
|
76
128
|
await this.info(preferences),
|
|
77
129
|
Zotero.getErrors(true).join('\n\n'),
|
|
78
130
|
Zotero.Debug.getConsoleViewerOutput().slice(-250000).join('\n'), // eslint-disable-line no-magic-numbers
|
|
79
131
|
].filter((txt) => txt).join('\n\n').trim();
|
|
80
|
-
|
|
132
|
+
bundler.add('debug.txt', log);
|
|
81
133
|
let rdf = await this.rdf();
|
|
82
134
|
if (rdf)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
86
|
-
if (typeof FormData === 'undefined' && Zotero.platformMajorVersion >= 102)
|
|
87
|
-
Components.utils.importGlobalProperties(['FormData']);
|
|
88
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
89
|
-
const zip = new Uint8Array(UZip.encode(files));
|
|
90
|
-
let blob;
|
|
91
|
-
let ext = '';
|
|
92
|
-
if (pubkey) {
|
|
93
|
-
try {
|
|
94
|
-
const subtle = Zotero.getMainWindow().crypto.subtle;
|
|
95
|
-
const pem = pubkey
|
|
96
|
-
.replace('-----BEGIN PUBLIC KEY-----', '')
|
|
97
|
-
.replace('-----END PUBLIC KEY-----', '')
|
|
98
|
-
.replace(/\s/g, '');
|
|
99
|
-
const keyBuffer = Uint8Array.from(atob(pem), c => c.charCodeAt(0)).buffer;
|
|
100
|
-
const publicKey = await subtle.importKey('spki', keyBuffer, { name: 'RSA-OAEP', hash: 'SHA-256' }, true, ['encrypt']);
|
|
101
|
-
const encrypted = await subtle.encrypt({ name: 'RSA-OAEP' }, publicKey, zip);
|
|
102
|
-
blob = new Blob([encrypted], { type: 'application/octet-stream' });
|
|
103
|
-
ext = '.enc';
|
|
104
|
-
}
|
|
105
|
-
catch (err) {
|
|
106
|
-
Services.prompt.alert(null, `Log encryption for ${plugin} failed`, err.message);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
if (!blob)
|
|
110
|
-
blob = new Blob([zip], { type: 'application/zip' });
|
|
135
|
+
bundler.add('items.rdf', rdf, true);
|
|
136
|
+
const blob = new Blob([bundler.zip], { type: 'application/zip' });
|
|
111
137
|
const formData = new FormData();
|
|
112
|
-
formData.append('file', blob,
|
|
138
|
+
formData.append('file', blob, bundler.name);
|
|
113
139
|
formData.append('expire', `${7 * 24}`);
|
|
114
140
|
try {
|
|
115
141
|
const response = await fetch('https://0x0.st', {
|
|
116
142
|
method: 'POST',
|
|
117
143
|
body: formData,
|
|
118
144
|
headers: {
|
|
119
|
-
'User-Agent':
|
|
145
|
+
'User-Agent': `Zotero-plugin/${pkg.version}`,
|
|
120
146
|
},
|
|
121
147
|
});
|
|
122
148
|
const body = await response.text();
|
|
123
149
|
const id = body.match(/https:\/\/0x0.st\/([A-Z0-9]+)\.zip/i);
|
|
124
150
|
if (!id)
|
|
125
151
|
throw new Error(body);
|
|
126
|
-
Services.prompt.alert(null, `Debug log ID for ${plugin}`,
|
|
152
|
+
Services.prompt.alert(null, `Debug log ID for ${plugin}`, bundler.id(`0x0-${id[1]}`));
|
|
127
153
|
}
|
|
128
154
|
catch (err) {
|
|
129
155
|
Services.prompt.alert(null, `Could not post debug log for ${plugin}`, err.message);
|