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.
@@ -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 cryptography.hazmat.primitives.asymmetric import padding
9
- from cryptography.hazmat.primitives import hashes
10
- from cryptography.hazmat.primitives.serialization import load_pem_private_key
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
- local, host, remote = sys.argv[1].split('-')
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
- print('unexpected debug log host', host)
16
- sys.exit(1)
30
+ oops('unexpected debug log host', host)
17
31
 
18
- encrypted = ('.') in remote
19
- remote = remote.split('.')[0]
20
- ext = '.enc' if encrypted else '.zip'
21
- url = f'https://0x0.com/{remote}{ext}'
22
- return SimpleNamespace(local=local, host=host, remote=remote, encrypted=encrypted, ext=ext, url=url)
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.downloaded = f'logs/{debuglog.local}{debuglog.ext}'
26
- print(debuglog.url, '=>', debuglog.downloaded)
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.downloaded)
31
-
32
- def decrypt():
33
- if not debuglog.encrypted:
34
- return
35
-
36
- encrypted = debuglog.downloaded
37
- debuglog.downloaded = f'logs/{debuglog.local}.zip'
38
-
39
- with open(sys.argv[2], 'rb') as f:
40
- private_key = load_pem_private_key(f.read(), password=None)
41
- with open(encrypted, 'rb') as f:
42
- encrypted_data = f.read()
43
- with open(debuglog.downloaded, 'wb') as f:
44
- f.write(private_key.decrypt(encrypted_data, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)))
45
-
46
- def show():
47
- with ZipFile(debuglod.downloaded) as f:
48
- for name in f.namelist():
49
- if not '/' in name:
50
- print('Unexpected', name, 'in', sys.argv[1])
51
- sys.exit(1)
52
- print(name)
53
- f.extractall(path='logs')
54
- os.remove(debuglog.downloaded)
55
- print(debuglog.local)
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.27",
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 files = {};
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
- files[`${key}/debug.txt`] = enc.encode(log);
132
+ bundler.add('debug.txt', log);
81
133
  let rdf = await this.rdf();
82
134
  if (rdf)
83
- files[`${key}/items.rdf`] = enc.encode(rdf);
84
- // do this runtime because Zotero is not defined at start for bootstrapped zoter6 plugins
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, `${key}${ext || '.zip'}`);
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': 'curl/8.7.1',
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}`, `${key}-0x0-${id[1]}{ext}`);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zotero-plugin",
3
- "version": "5.0.27",
3
+ "version": "5.0.29",
4
4
  "description": "Zotero plugin builder",
5
5
  "homepage": "https://github.com/retorquere/zotero-plugin/wiki",
6
6
  "bin": {