CH-J Toolsspráva serverov nad SSH
Menu
Uverejnený zdroj

CH-J Tools

Prechádzať adresáre a súbory pre konkrétne vydanie aplikácie.

Stiahnuť zdroj ZIP
CH-J / Server Manager 1704 súbory
0.1.2-beta Pridané: · Postavený:
quick.js Pridané: Dátum nezaznamenaný 22,1 KB · 301 riadky
Kód Stiahnuť súbor
1/* Quick-action window: verify or sign one file handed over by the system file manager.
2 All checks run in the Core; this page only shows the state and passes the user's choices. */
3"use strict";
4(() => {
5 const api = window.quickApi;
6 const $ = (id) => document.getElementById(id);
7 const T = {
8 cs: {
9 issuer: "Otisk klíče z podpisu", expectedFp: "Očekávaný otisk klíče (nepovinné, např. z webu vydavatele)", fromKeyserver: "Stáhnout klíč z keys.ch-j.de",
10 sourceKeyserver: "keys.ch-j.de (stažen, neuložen)", notOnKeyserver: "Na keys.ch-j.de pro tento otisk žádný veřejný klíč není.", fpMismatch: "Podpis je platný, ale vytvořil ho jiný klíč než zadaný otisk.",
11 fpMatched: "Shoduje se se zadaným otiskem", fpInvalid: "Zadejte celý otisk (40 nebo 64 šestnáctkových znaků).",
12 useKeyFile: "Ověřit veřejným klíčem ze souboru…", sourceFile: "Veřejný klíč ze souboru (neuložen)", keyFile: "Soubor klíče",
13 changed: "Podpis neodpovídá souboru: soubor nebo podpis se po podepsání změnil.", wrongKey: "Tento podpis nevytvořil vybraný klíč. Vyberte veřejný klíč podepisujícího.",
14 verifyTitle: "Ověření podpisu", signTitle: "Podepsání souboru", loading: "Načítání…",
15 signature: "Podpis", noSignature: "Vedle souboru nebyl nalezen podpis (.asc, .sig, .gpg).", size: "Velikost",
16 unlockVerify: "Keychain je zamčený. Ověřeno jen veřejnými klíči uloženými v modulu Hash. Odemkněte Keychain pro ověření i uloženými identitami.",
17 unlockSign: "Pro podepsání odemkněte Keychain heslem trezoru.", unlock: "Odemknout", vaultPassword: "Heslo trezoru",
18 needsSetup: "Trezor ještě neexistuje. Vytvořte ho v aplikaci a uložte do Keychainu klíč OpenPGP.",
19 keysTitle: "Podpisový klíč", noKeys: "V Keychainu není žádný klíč OpenPGP, který by mohl podepisovat (s tajnou částí, neodvolaný a platný).",
20 primary: "Hlavní klíč", subkey: "Podklíč", automatic: "Automatický výběr (podle formátu klíče)", expires: "platí do", noExpiry: "bez expirace",
21 passphrase: "Heslo klíče", overwrite: "Nahradit existující podpis", sign: "Podepsat", signing: "Podepisuji…", verifying: "Ověřuji…",
22 signed: "Soubor je podepsaný", signedTo: "Podpis uložen", valid: "Podpis je platný", invalid: "Podpis není platný",
23 signer: "Podepsal", fingerprint: "Otisk klíče", signingKey: "Podpisový klíč", created: "Vytvořeno", source: "Zdroj klíče",
24 sourceKeychain: "Keychain (uložená identita)", sourceSaved: "Uložený veřejný klíč (Hash)",
25 trustNote: "Platný podpis potvrzuje, že soubor od podpisu nezměnil držitel tohoto klíče. Neověřuje identitu osoby; stav odvolání klíče na keys.ch-j.de je uveden výše.",
26 keyMissing: "Podepisující klíč není v Keychainu, mezi uloženými klíči ani na keys.ch-j.de. Zadejte jeho otisk nebo vyberte soubor s veřejným klíčem.",
27 openMain: "Otevřít aplikaci", close: "Zavřít", warnRevoked: "Klíč je odvolaný.", warnExpired: "Platnost klíče vypršela.", pq: "PQ",
28 keyserver: "keys.ch-j.de", serverPublished: "Zveřejněn", serverRevoked: "Odvolán na serveru", serverMissing: "Na serveru není", serverUnknown: "Nezjištěno",
29 checkedNow: "právě ověřeno", checkedAt: "ověřeno", serverStale: "uložený stav, server teď nedostupný", warnServerRevoked: "Keyserver hlásí klíč jako odvolaný."
30 },
31 de: {
32 issuer: "Schlüssel-Fingerabdruck der Signatur", expectedFp: "Erwarteter Fingerabdruck (optional, z. B. von der Website des Herausgebers)", fromKeyserver: "Schlüssel von keys.ch-j.de laden",
33 sourceKeyserver: "keys.ch-j.de (geladen, nicht gespeichert)", notOnKeyserver: "keys.ch-j.de hat keinen öffentlichen Schlüssel für diesen Fingerabdruck.", fpMismatch: "Die Signatur ist gültig, stammt aber von einem anderen Schlüssel als dem eingegebenen Fingerabdruck.",
34 fpMatched: "Stimmt mit dem eingegebenen Fingerabdruck überein", fpInvalid: "Geben Sie den vollständigen Fingerabdruck ein (40 oder 64 Hexadezimalzeichen).",
35 useKeyFile: "Mit öffentlichem Schlüssel aus Datei prüfen…", sourceFile: "Öffentlicher Schlüssel aus Datei (nicht gespeichert)", keyFile: "Schlüsseldatei",
36 changed: "Die Signatur passt nicht zur Datei: Datei oder Signatur wurden nach dem Signieren geändert.", wrongKey: "Diese Signatur stammt nicht vom gewählten Schlüssel. Wählen Sie den öffentlichen Schlüssel des Signierenden.",
37 verifyTitle: "Signatur prüfen", signTitle: "Datei signieren", loading: "Wird geladen…",
38 signature: "Signatur", noSignature: "Neben der Datei wurde keine Signatur gefunden (.asc, .sig, .gpg).", size: "Größe",
39 unlockVerify: "Der Schlüsselbund ist gesperrt. Geprüft wurde nur mit im Modul Hash gespeicherten öffentlichen Schlüsseln. Entsperren Sie ihn, um auch gespeicherte Identitäten zu verwenden.",
40 unlockSign: "Zum Signieren den Schlüsselbund mit dem Tresorpasswort entsperren.", unlock: "Entsperren", vaultPassword: "Tresorpasswort",
41 needsSetup: "Es gibt noch keinen Tresor. Legen Sie ihn in der Anwendung an und speichern Sie einen OpenPGP-Schlüssel.",
42 keysTitle: "Signaturschlüssel", noKeys: "Im Schlüsselbund ist kein OpenPGP-Schlüssel, der signieren kann (mit geheimem Teil, nicht widerrufen, gültig).",
43 primary: "Hauptschlüssel", subkey: "Unterschlüssel", automatic: "Automatische Auswahl (je nach Schlüsselformat)", expires: "gültig bis", noExpiry: "ohne Ablauf",
44 passphrase: "Schlüsselpasswort", overwrite: "Vorhandene Signatur ersetzen", sign: "Signieren", signing: "Signiere…", verifying: "Prüfe…",
45 signed: "Datei signiert", signedTo: "Signatur gespeichert", valid: "Signatur gültig", invalid: "Signatur ungültig",
46 signer: "Signiert von", fingerprint: "Fingerabdruck", signingKey: "Signaturschlüssel", created: "Erstellt", source: "Schlüsselquelle",
47 sourceKeychain: "Schlüsselbund (gespeicherte Identität)", sourceSaved: "Gespeicherter öffentlicher Schlüssel (Hash)",
48 trustNote: "Eine gültige Signatur bestätigt, dass die Datei seit dem Signieren durch den Inhaber dieses Schlüssels unverändert ist. Sie bestätigt nicht die Identität der Person; der Widerrufsstatus auf keys.ch-j.de steht oben.",
49 keyMissing: "Der signierende Schlüssel ist weder im Schlüsselbund, unter den gespeicherten Schlüsseln noch auf keys.ch-j.de. Geben Sie seinen Fingerabdruck ein oder wählen Sie eine Datei mit dem öffentlichen Schlüssel.",
50 openMain: "Anwendung öffnen", close: "Schließen", warnRevoked: "Der Schlüssel ist widerrufen.", warnExpired: "Der Schlüssel ist abgelaufen.", pq: "PQ",
51 keyserver: "keys.ch-j.de", serverPublished: "Veröffentlicht", serverRevoked: "Auf dem Server widerrufen", serverMissing: "Nicht auf dem Server", serverUnknown: "Nicht ermittelt",
52 checkedNow: "gerade geprüft", checkedAt: "geprüft", serverStale: "gespeicherter Stand, Server gerade nicht erreichbar", warnServerRevoked: "Der Keyserver meldet den Schlüssel als widerrufen."
53 },
54 en: {
55 issuer: "Key fingerprint in the signature", expectedFp: "Expected key fingerprint (optional, e.g. from the publisher's website)", fromKeyserver: "Get the key from keys.ch-j.de",
56 sourceKeyserver: "keys.ch-j.de (downloaded, not saved)", notOnKeyserver: "keys.ch-j.de has no public key for this fingerprint.", fpMismatch: "The signature is valid, but it was made with a different key than the fingerprint you entered.",
57 fpMatched: "Matches the fingerprint you entered", fpInvalid: "Enter the full fingerprint (40 or 64 hexadecimal characters).",
58 useKeyFile: "Verify with a public key file…", sourceFile: "Public key from a file (not saved)", keyFile: "Key file",
59 changed: "The signature does not match the file: the file or the signature was changed after signing.", wrongKey: "This signature was not made with the chosen key. Choose the signer's public key.",
60 verifyTitle: "Verify signature", signTitle: "Sign file", loading: "Loading…",
61 signature: "Signature", noSignature: "No signature (.asc, .sig, .gpg) was found next to the file.", size: "Size",
62 unlockVerify: "The Keychain is locked. Only public keys saved in the Hash module were used. Unlock the Keychain to check stored identities too.",
63 unlockSign: "Unlock the Keychain with the vault password to sign.", unlock: "Unlock", vaultPassword: "Vault password",
64 needsSetup: "There is no vault yet. Create it in the application and store an OpenPGP key in the Keychain.",
65 keysTitle: "Signing key", noKeys: "The Keychain has no OpenPGP key that can sign (with a secret part, not revoked, valid).",
66 primary: "Primary key", subkey: "Subkey", automatic: "Automatic choice (by key format)", expires: "valid until", noExpiry: "no expiry",
67 passphrase: "Key passphrase", overwrite: "Replace the existing signature", sign: "Sign", signing: "Signing…", verifying: "Verifying…",
68 signed: "File signed", signedTo: "Signature saved", valid: "Signature is valid", invalid: "Signature is not valid",
69 signer: "Signed by", fingerprint: "Key fingerprint", signingKey: "Signing key", created: "Created", source: "Key source",
70 sourceKeychain: "Keychain (stored identity)", sourceSaved: "Saved public key (Hash)",
71 trustNote: "A valid signature confirms that the holder of this key signed the file and it has not changed since. It does not confirm the person's identity; the revocation status on keys.ch-j.de is shown above.",
72 keyMissing: "The signing key is not in the Keychain, among the saved keys or on keys.ch-j.de. Enter its fingerprint or choose a public key file.",
73 openMain: "Open application", close: "Close", warnRevoked: "The key is revoked.", warnExpired: "The key has expired.", pq: "PQ",
74 keyserver: "keys.ch-j.de", serverPublished: "Published", serverRevoked: "Revoked on the server", serverMissing: "Not on the server", serverUnknown: "Not determined",
75 checkedNow: "checked just now", checkedAt: "checked", serverStale: "stored state, server unreachable now", warnServerRevoked: "The keyserver reports the key as revoked."
76 }
77 };
78 let lang = (navigator.language || "en").slice(0, 2);
79 const t = (key) => (T[lang] || T.en)[key] || T.en[key] || key;
80 const ICONS = {
81 verify: '<svg viewBox="0 0 24 24"><path d="M12 3l7 3v5c0 4.5-3 8.3-7 10-4-1.7-7-5.5-7-10V6l7-3z"/><path d="M9 12l2 2 4-4"/></svg>',
82 sign: '<svg viewBox="0 0 24 24"><path d="M4 20h16"/><path d="M14.5 4.5l5 5L9 20H4v-5L14.5 4.5z"/></svg>',
83 bad: '<svg viewBox="0 0 24 24"><path d="M12 3l7 3v5c0 4.5-3 8.3-7 10-4-1.7-7-5.5-7-10V6l7-3z"/><path d="M10 10l4 4M14 10l-4 4"/></svg>'
84 };
85 let state = null;
86 let chosen = null;
88 const message = (error) => String(error && error.message || error).replace(/^.*Error: /s, "");
89 const showError = (error) => { $("error").textContent = error ? message(error) : ""; };
90 const group = (fp) => String(fp || "").replace(/(.{4})/g, "$1 ").trim();
91 const size = (n) => n < 1024 ? `${n} B` : n < 1048576 ? `${(n / 1024).toFixed(1)} KiB` : `${(n / 1048576).toFixed(1)} MiB`;
92 const date = (value) => {
93 if (value === null || value === undefined || value === "") return "";
94 const d = typeof value === "number" ? new Date(value * 1000) : new Date(value);
95 return isNaN(d) ? String(value) : d.toLocaleString(lang);
96 };
97 const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text !== undefined) n.textContent = text; return n; };
98 const line = (label, value, mono) => { const row = el("div", "line"); row.append(el("span", "", label)); const v = el("span", mono ? "mono" : "", value); row.append(v); return row; };
99 const busy = (on) => document.body.classList.toggle("busy", on);
101 function badge(kind) {
102 $("badge").className = "badge" + (kind === "ok" ? " ok" : kind === "bad" ? " bad" : "");
103 $("badge").innerHTML = kind === "bad" ? ICONS.bad : ICONS[state && state.action === "sign" ? "sign" : "verify"];
104 }
106 function texts() {
107 document.documentElement.lang = lang;
108 const title = state && state.action === "sign" ? t("signTitle") : state ? t("verifyTitle") : t("loading");
109 $("title").textContent = title;
110 document.title = `CH-J — ${title}`;
111 $("openMain").textContent = t("openMain");
112 $("close").textContent = t("close");
113 $("unlockButton").textContent = t("unlock");
114 $("vaultPassword").placeholder = t("vaultPassword");
115 $("keysTitle").textContent = t("keysTitle");
116 $("passphraseLabel").textContent = t("passphrase");
117 $("overwriteLabel").textContent = t("overwrite");
118 $("signButton").textContent = t("sign");
119 }
121 function renderFile() {
122 const file = state.file;
123 $("fileBox").hidden = !file;
124 if (!file) return;
125 $("fileName").textContent = file.name;
126 $("filePath").textContent = file.path;
127 const extra = [`${t("size")}: ${size(file.size)}`];
128 if (state.action === "verify") extra.push(state.signature ? `${t("signature")}: ${state.signature.name}` : t("noSignature"));
129 if (state.action === "sign" && state.target) extra.push(`→ ${state.target.name}`);
130 $("fileExtra").textContent = extra.join(" · ");
131 }
133 function renderUnlock(hint) {
134 const vault = state.vault || {};
135 if (vault.needsSetup) { $("unlockBox").hidden = false; $("unlockForm").hidden = true; $("unlockHint").textContent = t("needsSetup"); return; }
136 $("unlockForm").hidden = false;
137 $("unlockBox").hidden = vault.unlocked || !vault.initialized;
138 $("unlockHint").textContent = hint;
139 if (!$("unlockBox").hidden) $("vaultPassword").focus();
140 }
142 function renderKeys() {
143 const box = $("keys");
144 box.replaceChildren();
145 $("keysBox").hidden = !(state.vault && state.vault.unlocked);
146 if ($("keysBox").hidden) return;
147 const keys = state.keys || [];
148 if (!keys.length) { box.append(el("p", "hint", t("noKeys"))); $("signForm").hidden = true; return; }
149 $("signForm").hidden = false;
150 $("overwriteRow").hidden = !(state.target && state.target.exists);
151 let first = null;
152 for (const key of keys) {
153 const card = el("div", "identity");
154 const uid = (key.userIds || []).map((u) => u.userId || u).filter(Boolean)[0];
155 card.append(el("div", "identity-head", key.label || uid || group(key.fingerprint)));
156 card.append(el("div", "identity-sub", [uid && uid !== key.label ? uid : "", `V${key.version || "?"}`].filter(Boolean).join(" · ")));
157 for (const component of key.components) {
158 const row = el("label", "component");
159 const radio = el("input");
160 radio.type = "radio"; radio.name = "component";
161 radio.value = JSON.stringify({ id: key.id, component: component.fingerprint || null });
162 radio.addEventListener("change", () => { chosen = JSON.parse(radio.value); $("signButton").disabled = false; $("keyPassphrase").focus(); });
163 const meta = el("span", "meta");
164 const role = el("span", "role", component.role === "primary" ? t("primary") : component.role === "subkey" ? t("subkey") : t("automatic"));
165 if (component.algorithm) role.append(el("span", "tag", String(component.algorithm)));
166 if (component.postQuantum) role.append(el("span", "tag", t("pq")));
167 meta.append(role);
168 if (component.fingerprint) meta.append(el("span", "fp mono", group(component.fingerprint)));
169 meta.append(el("span", "fp", component.expiresAt ? `${t("expires")} ${date(component.expiresAt)}` : t("noExpiry")));
170 row.append(radio, meta);
171 card.append(row);
172 first = first || radio;
173 }
174 box.append(card);
175 }
176 // One identity with one signing component: preselect it.
177 if (keys.length === 1 && keys[0].components.length === 1 && first) { first.checked = true; first.dispatchEvent(new Event("change")); }
178 }
180 function showResult(kind, title, lines, note) {
181 const box = $("result");
182 box.className = `result ${kind}`;
183 box.replaceChildren(el("h2", "", title), ...lines);
184 if (note) box.append(el("p", "note", note));
185 box.hidden = false;
186 badge(kind === "ok" ? "ok" : kind === "bad" ? "bad" : "");
187 }
189 function showVerified(r) {
190 const uid = (r.userIds || []).map((u) => u.userId || u).filter(Boolean).join(", ");
191 const lines = [];
192 if (uid || r.label) lines.push(line(t("signer"), uid || r.label));
193 lines.push(line(t("fingerprint"), group(r.primaryFingerprint), true));
194 if (r.signerFingerprint && r.signerFingerprint !== r.primaryFingerprint) lines.push(line(t("signingKey"), group(r.signerFingerprint), true));
195 if (r.createdAt) lines.push(line(t("created"), date(r.createdAt)));
196 lines.push(line(t("source"), r.keySource === "keychain" ? t("sourceKeychain") : r.keySource === "file" ? t("sourceFile") : r.keySource === "keyserver" ? t("sourceKeyserver") : t("sourceSaved")));
197 if (r.expectedMatched === true || r.fingerprintMatched === true) lines.push(line(t("fingerprint"), t("fpMatched")));
198 if (r.keyFile) lines.push(line(t("keyFile"), r.keyFile));
199 // Keyserver state chosen by freshness in the Core (stored if fresh enough, otherwise checked now).
200 const s = r.server;
201 if (s && s.known) {
202 const status = s.found !== true && s.found !== false ? t("serverUnknown") : s.revoked === true ? t("serverRevoked") : s.found ? t("serverPublished") : t("serverMissing");
203 const when = s.live ? t("checkedNow") : s.verifiedAt ? `${t("checkedAt")} ${date(s.verifiedAt)}` : "";
204 lines.push(line(t("keyserver"), [status, when, s.fresh === false ? t("serverStale") : ""].filter(Boolean).join(" · ")));
205 }
206 const warn = [r.revoked === true ? t("warnRevoked") : "", s && s.revoked === true && r.revoked !== true ? t("warnServerRevoked") : "", r.expired === true ? t("warnExpired") : ""].filter(Boolean).join(" ");
207 showResult(warn ? "warn" : "ok", t("valid"), lines, [warn, t("trustNote")].filter(Boolean).join(" "));
208 }
210 // Failure text and whether another public key can still help (unknown signer or a wrong chosen key).
211 function failure(error) {
212 const text = message(error);
213 if (/QUICK_SIGNATURE_INVALID|does not match this file/i.test(text)) return { text: t("changed"), keyHelps: false };
214 if (/FINGERPRINT_MISMATCH|different key than the fingerprint/i.test(text)) return { text: t("fpMismatch"), keyHelps: true };
215 if (/FINGERPRINT_INVALID|full fingerprint/i.test(text)) return { text: t("fpInvalid"), keyHelps: true };
216 if (/KEYSERVER_NOT_FOUND|no public key for this fingerprint/i.test(text)) return { text: t("notOnKeyserver"), keyHelps: true };
217 if (/SIGNER_UNKNOWN|not made with/i.test(text)) return { text: t("wrongKey"), keyHelps: true };
218 if (/fingerprint alone|key required|KEY_REQUIRED/i.test(text)) return { text: t("keyMissing"), keyHelps: true };
219 return { text, keyHelps: false };
220 }
222 function showFailure(error, typed) {
223 const { text, keyHelps } = failure(error);
224 const issuer = (state.issuers || []).find((i) => i.length === 40 || i.length === 64) || (state.issuers || [])[0];
225 showResult("bad", t("invalid"), issuer ? [line(t("issuer"), group(issuer), true)] : [], text);
226 if (!keyHelps) return;
227 const box = el("div", "key-options");
228 const label = el("label", "expected", t("expectedFp"));
229 const input = el("input");
230 input.id = "expectedFingerprint"; input.className = "mono"; input.autocomplete = "off"; input.spellcheck = false;
231 input.value = typed || "";
232 label.append(input);
233 const fromServer = el("button", "button", t("fromKeyserver"));
234 fromServer.type = "button"; fromServer.id = "verifyKeyserver";
235 fromServer.addEventListener("click", () => verifyWith("keyserver", input.value));
236 const fromFile = el("button", "button", t("useKeyFile"));
237 fromFile.type = "button"; fromFile.id = "verifyWithKey";
238 fromFile.addEventListener("click", () => verifyWith("file", input.value));
239 const row = el("div", "row");
240 row.append(fromServer, fromFile);
241 box.append(label, row);
242 $("result").append(box);
243 }
245 async function verifyWith(source, typed) {
246 showError(null); busy(true);
247 try {
248 const r = source === "keyserver" ? await api.verifyWithKeyserver(typed) : await api.verifyWithKey(typed);
249 if (r && r.cancelled) return;
250 showVerified(r);
251 } catch (error) { showFailure(error, typed); }
252 finally { busy(false); }
253 }
255 async function verify() {
256 if (!state.signature) { showResult("bad", t("invalid"), [], t("noSignature")); return; }
257 showResult("", t("verifying"), []);
258 try { showVerified(await api.verify()); }
259 catch (error) { showFailure(error); }
260 renderUnlock(t("unlockVerify"));
261 }
263 async function load() {
264 showError(null);
265 try {
266 state = await api.state();
267 lang = T[state.language] ? state.language : T[String(state.language || "").slice(0, 2)] ? String(state.language).slice(0, 2) : "en";
268 texts(); badge(""); renderFile();
269 if (state.action === "verify") { $("keysBox").hidden = true; await verify(); }
270 else { renderUnlock(t("unlockSign")); renderKeys(); }
271 } catch (error) { texts(); badge("bad"); showError(error); }
272 }
274 $("unlockForm").addEventListener("submit", async (event) => {
275 event.preventDefault();
276 showError(null); busy(true);
277 try { await api.unlock($("vaultPassword").value); $("vaultPassword").value = ""; await load(); }
278 catch (error) { showError(error); $("vaultPassword").select(); }
279 finally { busy(false); }
280 });
282 $("signForm").addEventListener("submit", async (event) => {
283 event.preventDefault();
284 if (!chosen) return;
285 showError(null); busy(true);
286 $("signButton").textContent = t("signing");
287 try {
288 const r = await api.sign({ id: chosen.id, component: chosen.component, passphrase: $("keyPassphrase").value, overwrite: $("overwrite").checked });
289 $("keyPassphrase").value = "";
290 $("keysBox").hidden = true; $("unlockBox").hidden = true;
291 showResult("ok", t("signed"), [line(t("signedTo"), r.signature.path, true)]);
292 } catch (error) { showError(error); $("keyPassphrase").select(); }
293 finally { busy(false); $("signButton").textContent = t("sign"); }
294 });
296 $("close").addEventListener("click", () => api.close());
297 $("openMain").addEventListener("click", () => api.openMain().catch(showError));
298 document.addEventListener("keydown", (event) => { if (event.key === "Escape") api.close(); });
299 texts();
300 load();
301})();
SHA-256a05a0cf7854a49c430d95332670b9fffb264370dc217624761d93b887862acc5

CH-J Proprietary Software License 1.14 · Zdroj sa poskytuje v rámci CH-J Proprietary Software License 1.14. Jeho dostupnosť nemení licenčné podmienky ani neposkytuje dodatočné práva.

Archív SHA-256ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
Súbory