Bug 1518172 - Import ctypes-otr, updated by kaie/aleca. r=florian,clokep,mkmelin
Original MPL v2 code by Arlo Breault from https://github.com/arlolra/ctypes-otr/ Ported to Thunderbird by Kai Engert, includes UI changes by Alessandro Castellani.
This commit is contained in:
Родитель
74574f3b34
Коммит
f20cf4817a
|
@ -12,3 +12,19 @@ chat.jar:
|
|||
* content/chat/imtooltip.xml
|
||||
content/chat/conversation-browser.js
|
||||
content/chat/conv.html
|
||||
content/chat/otr-add-fingerprint.js
|
||||
content/chat/otr-add-fingerprint.xul
|
||||
content/chat/otr-auth.js
|
||||
content/chat/otr-auth.xul
|
||||
content/chat/otr-generate-key.js
|
||||
content/chat/otr-generate-key.xul
|
||||
content/chat/otrWorker.js
|
||||
content/chat/otr-auth.dtd
|
||||
content/chat/otr-auth.properties
|
||||
content/chat/otr-add-finger.dtd
|
||||
content/chat/otr-add-finger.properties
|
||||
content/chat/otr.properties
|
||||
content/chat/otr-generate-key.dtd
|
||||
content/chat/otr-generate-key.properties
|
||||
content/chat/otrUI.properties
|
||||
content/chat/otr-chat.dtd
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<!ENTITY addFingerDialog.title "Add Fingerprint">
|
||||
<!ENTITY addFingerDialog.finger "Fingerprint">
|
||||
<!ENTITY addFingerDialog.cancel "Skip">
|
||||
<!-- LOCALIZATION NOTE (addFingerDialog.tooltip): do not translate OTR which is the name of an encryption protocol -->
|
||||
<!ENTITY addFingerDialog.tooltip "If you know the 40 hex char fingerprint of your contact's OTR private key, enter it now.">
|
|
@ -0,0 +1,6 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
# LOCALIZATION NOTE (addfinger.title): %S is the name of a chat contact person
|
||||
addfinger.title=Enter the fingerprint of the OTR key used by %S
|
|
@ -0,0 +1,50 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const {
|
||||
XPCOMUtils,
|
||||
l10nHelper,
|
||||
} = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
|
||||
const {OTR} = ChromeUtils.import("resource:///modules/OTR.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "_", () =>
|
||||
l10nHelper("chrome://chat/content/otr-add-finger.properties")
|
||||
);
|
||||
|
||||
var args = window.arguments[0].wrappedJSObject;
|
||||
|
||||
var otrAddFinger = {
|
||||
onload() {
|
||||
document.title = _("addfinger.title", args.screenname);
|
||||
|
||||
document.addEventListener("dialogaccept", () => {
|
||||
return this.add();
|
||||
});
|
||||
},
|
||||
|
||||
oninput(e) {
|
||||
e.value = e.value.replace(/[^0-9a-fA-F]/gi, "");
|
||||
document.documentElement.getButton("accept").disabled = (e.value.length != 40);
|
||||
},
|
||||
|
||||
add(e) {
|
||||
let hex = document.getElementById("finger").value;
|
||||
let context = OTR.getContextFromRecipient(
|
||||
args.account,
|
||||
args.protocol,
|
||||
args.screenname
|
||||
);
|
||||
let finger = OTR.addFingerprint(context, hex);
|
||||
if (finger.isNull())
|
||||
return;
|
||||
try {
|
||||
// Ignore the return, this is just a test.
|
||||
OTR.getUIConvFromContext(context);
|
||||
} catch (error) {
|
||||
// We expect that a conversation may not have been started.
|
||||
context = null;
|
||||
}
|
||||
OTR.setTrust(finger, true, context);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" ?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css" ?>
|
||||
|
||||
<!DOCTYPE window SYSTEM "chrome://chat/content/otr-add-finger.dtd">
|
||||
|
||||
<dialog id="otrAddFingerDialog"
|
||||
windowtype="OTR:AddFinger"
|
||||
onload="otrAddFinger.onload()"
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
title="&addFingerDialog.title;"
|
||||
buttons="accept,cancel"
|
||||
buttonlabelcancel="&addFingerDialog.cancel;"
|
||||
buttondisabledaccept="true">
|
||||
|
||||
<script type="application/javascript" src="chrome://chat/content/otr-add-fingerprint.js"/>
|
||||
<vbox flex="1">
|
||||
<label value="&addFingerDialog.tooltip;" control="name" flex="1"/>
|
||||
<hbox id="fingerBox" align="baseline" flex="1">
|
||||
<label value="&addFingerDialog.finger;" control="name"/>
|
||||
<textbox id="finger" oninput="otrAddFinger.oninput(this)" flex="1"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
</dialog>
|
|
@ -0,0 +1,20 @@
|
|||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<!ENTITY authDialog.title "Verify contact's identity">
|
||||
<!ENTITY authDialog.authenticate "Verify">
|
||||
<!ENTITY authDialog.help "Help">
|
||||
<!ENTITY authDialog.yes "Yes">
|
||||
<!ENTITY authDialog.no "No">
|
||||
<!ENTITY authDialog.verified "I have verified that this is in fact the correct fingerprint.">
|
||||
<!ENTITY authDialog.manualVerification "Manual fingerprint verification">
|
||||
<!ENTITY authDialog.questionAndAnswer "Question and answer">
|
||||
<!ENTITY authDialog.sharedSecret "Shared secret">
|
||||
<!ENTITY authDialog.manualInstruction "To verify the fingerprint, contact your intended chat partner via some other authenticated channel, such as the telephone or GPG-signed email. Each of you should tell your fingerprint to the other. If everything matches up, you should indicate in the dialog below that you have verified the fingerprint.">
|
||||
<!ENTITY authDialog.how "How would you like to verify your contact's identity?">
|
||||
<!ENTITY authDialog.qaInstruction "To verify their identity, pick a question whose answer is known only to you and your contact. Enter this question and answer, then wait for your contact to enter the answer as well. If the answers do not match, then you may be talking to an imposter.">
|
||||
<!ENTITY authDialog.secretInstruction "To verify their identity, pick a secret known only to you and your contact. Enter this secret, then wait for your contact to enter it as well. If the secrets do not match, then you may be talking to an imposter.">
|
||||
<!ENTITY authDialog.question "Enter question here:">
|
||||
<!ENTITY authDialog.answer "Enter secret answer here (case sensitive):">
|
||||
<!ENTITY authDialog.secret "Enter secret here:">
|
|
@ -0,0 +1,175 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const {Services} = ChromeUtils.import("resource:///modules/imServices.jsm");
|
||||
const {
|
||||
XPCOMUtils,
|
||||
l10nHelper,
|
||||
} = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
|
||||
const {OTR} = ChromeUtils.import("resource:///modules/OTR.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "_", () =>
|
||||
l10nHelper("chrome://chat/content/otr-auth.properties")
|
||||
);
|
||||
|
||||
var [mode, uiConv, contactInfo] = window.arguments;
|
||||
|
||||
// This window implements the interactive authentication of a buddy's
|
||||
// key. At open time, we're given several parameters, and the "mode"
|
||||
// parameter tells us, from where we've been called.
|
||||
// mode == "pref" means that we have been opened from the preferences,
|
||||
// and it means we cannot rely on the other user being online, and
|
||||
// we there might be no uiConv active currently, so we fall back.
|
||||
|
||||
document.title = _("auth.title",
|
||||
(mode === "pref") ? contactInfo.screenname : uiConv.normalizedName);
|
||||
|
||||
function showSection(selected, hideMenu) {
|
||||
document.getElementById("how").hidden = !!hideMenu;
|
||||
[ "questionAndAnswer",
|
||||
"sharedSecret",
|
||||
"manualVerification",
|
||||
"ask",
|
||||
].forEach(function(key) {
|
||||
document.getElementById(key).hidden = (key !== selected);
|
||||
});
|
||||
window.sizeToContent();
|
||||
}
|
||||
|
||||
function startSMP(context, answer, question) {
|
||||
OTR.sendSecret(context, answer, question);
|
||||
OTR.authUpdate(context, 10);
|
||||
}
|
||||
|
||||
function manualVerification(fingerprint, context) {
|
||||
let opts = document.getElementById("verifiedOption");
|
||||
let trust = (opts.selectedItem.value === "yes");
|
||||
OTR.setTrust(fingerprint, trust, context);
|
||||
}
|
||||
|
||||
function populateFingers(context, theirs, trust) {
|
||||
let fingers = document.getElementById("fingerprints");
|
||||
let yours = OTR.privateKeyFingerprint(context.account, context.protocol);
|
||||
if (!yours)
|
||||
throw new Error("Fingerprint should already be generated.");
|
||||
fingers.value =
|
||||
_("auth.yourFingerprint", context.account, yours) + "\n\n" +
|
||||
_("auth.theirFingerprint", context.username, theirs);
|
||||
let opts = document.getElementById("verifiedOption");
|
||||
let verified = trust ? "yes" : "no";
|
||||
for (let item of opts.menupopup.childNodes) {
|
||||
if (verified === item.value) {
|
||||
opts.selectedItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var otrAuth = {
|
||||
onload() {
|
||||
document.addEventListener("dialogaccept", () => {
|
||||
return this.accept();
|
||||
});
|
||||
|
||||
document.addEventListener("dialogcancel", () => {
|
||||
return this.cancel();
|
||||
});
|
||||
|
||||
document.addEventListener("dialoghelp", () => {
|
||||
return this.help();
|
||||
});
|
||||
|
||||
let context, theirs;
|
||||
switch (mode) {
|
||||
case "start":
|
||||
context = OTR.getContext(uiConv.target);
|
||||
theirs = OTR.hashToHuman(context.fingerprint);
|
||||
populateFingers(context, theirs, context.trust);
|
||||
showSection("questionAndAnswer");
|
||||
break;
|
||||
case "pref":
|
||||
context = OTR.getContextFromRecipient(
|
||||
contactInfo.account,
|
||||
contactInfo.protocol,
|
||||
contactInfo.screenname
|
||||
);
|
||||
theirs = contactInfo.fingerprint;
|
||||
populateFingers(context, theirs, contactInfo.trust);
|
||||
showSection("manualVerification", true);
|
||||
this.oninput({ value: true });
|
||||
break;
|
||||
case "ask":
|
||||
document.getElementById("askLabel").textContent = contactInfo.question ?
|
||||
_("auth.question", contactInfo.question)
|
||||
: _("auth.secret");
|
||||
showSection("ask", true);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
accept() {
|
||||
// uiConv may not be present in pref mode
|
||||
let context = uiConv ? OTR.getContext(uiConv.target) : null;
|
||||
if (mode === "pref") {
|
||||
manualVerification(contactInfo.fpointer, context);
|
||||
} else if (mode === "start") {
|
||||
let how = document.getElementById("howOption");
|
||||
switch (how.selectedItem.value) {
|
||||
case "questionAndAnswer":
|
||||
let question = document.getElementById("question").value;
|
||||
let answer = document.getElementById("answer").value;
|
||||
startSMP(context, answer, question);
|
||||
break;
|
||||
case "sharedSecret":
|
||||
let secret = document.getElementById("secret").value;
|
||||
startSMP(context, secret);
|
||||
break;
|
||||
case "manualVerification":
|
||||
manualVerification(context.fingerprint, context);
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unreachable!");
|
||||
}
|
||||
} else if (mode === "ask") {
|
||||
let response = document.getElementById("response").value;
|
||||
OTR.sendResponse(context, response);
|
||||
OTR.authUpdate(context, contactInfo.progress);
|
||||
} else {
|
||||
throw new Error("Unreachable!");
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
cancel() {
|
||||
if (mode === "ask") {
|
||||
let context = OTR.getContext(uiConv.target);
|
||||
OTR.abortSMP(context);
|
||||
}
|
||||
},
|
||||
|
||||
oninput(e) {
|
||||
document.documentElement.getButton("accept").disabled = !e.value;
|
||||
},
|
||||
|
||||
how() {
|
||||
let how = document.getElementById("howOption").selectedItem.value;
|
||||
switch (how) {
|
||||
case "questionAndAnswer":
|
||||
this.oninput(document.getElementById("answer"));
|
||||
break;
|
||||
case "sharedSecret":
|
||||
this.oninput(document.getElementById("secret"));
|
||||
break;
|
||||
case "manualVerification":
|
||||
this.oninput({ value: true });
|
||||
break;
|
||||
}
|
||||
showSection(how);
|
||||
},
|
||||
|
||||
help() {
|
||||
Services.prompt.alert(window, _("auth.helpTitle"), _("auth.help"));
|
||||
},
|
||||
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
# LOCALIZATION NOTE (auth.title): %S is the screen name of a chat contact person
|
||||
auth.title=Verify the identity of %S
|
||||
# LOCALIZATION NOTE (auth.yourFingerprint): 1st %S is the user's own screen name. 2nd %S is the fingerprint (a checksum) of the user's own encryption key.
|
||||
auth.yourFingerprint=Fingerprint for you, %S:\n%S
|
||||
# LOCALIZATION NOTE (auth.theirFingerprint): 1st %S is the screen name of a chat contact. 2nd %S is the fingerprint (a checksum) of the chat contact's encryption key.
|
||||
auth.theirFingerprint=Purported fingerprint for %S:\n%S
|
||||
auth.help=Verifying a contact's identity helps ensure that the person you are talking to is who they claim to be.
|
||||
auth.helpTitle=Verification help
|
||||
# LOCALIZATION NOTE (auth.question): %S is a question (any text is possible) that was received from a chat contact
|
||||
auth.question=This is the question asked by your contact:\n\n%S\n\nEnter secret answer here (case sensitive):
|
||||
auth.secret=Enter secret here:
|
|
@ -0,0 +1,71 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css" ?>
|
||||
|
||||
<!DOCTYPE window SYSTEM "chrome://chat/content/otr-auth.dtd">
|
||||
|
||||
<dialog
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
xmlns:html="http://www.w3.org/1999/xhtml"
|
||||
id="otrAuthDialog"
|
||||
windowtype="OTR:Auth"
|
||||
title="&authDialog.title;"
|
||||
onload="otrAuth.onload()"
|
||||
buttons="accept,cancel,help"
|
||||
buttonlabelaccept="&authDialog.authenticate;"
|
||||
buttonlabelhelp="&authDialog.help;"
|
||||
buttondisabledaccept="true">
|
||||
|
||||
<script type="application/javascript" src="chrome://chat/content/otr-auth.js" />
|
||||
|
||||
<groupbox id="how" hidden="true">
|
||||
<caption label="&authDialog.how;"/>
|
||||
<menulist id="howOption" oncommand="otrAuth.how();">
|
||||
<menupopup>
|
||||
<menuitem label="&authDialog.questionAndAnswer;" value="questionAndAnswer" />
|
||||
<menuitem label="&authDialog.sharedSecret;" value="sharedSecret" />
|
||||
<menuitem label="&authDialog.manualVerification;" value="manualVerification" />
|
||||
</menupopup>
|
||||
</menulist>
|
||||
</groupbox>
|
||||
|
||||
<groupbox id="questionAndAnswer" hidden="true">
|
||||
<caption label="&authDialog.questionAndAnswer;" />
|
||||
<description style="width: 300px; white-space: pre-wrap;">&authDialog.qaInstruction;</description>
|
||||
<label value="&authDialog.question;" control="question" flex="1" />
|
||||
<textbox id="question" />
|
||||
<label value="&authDialog.answer;" control="answer" flex="1" />
|
||||
<textbox id="answer" oninput="otrAuth.oninput(this)" />
|
||||
</groupbox>
|
||||
|
||||
<groupbox id="sharedSecret" hidden="true">
|
||||
<caption label="&authDialog.sharedSecret;" />
|
||||
<description style="width: 300px; white-space: pre-wrap;">&authDialog.secretInstruction;</description>
|
||||
<label value="&authDialog.secret;" control="secret" flex="1" />
|
||||
<textbox id="secret" oninput="otrAuth.oninput(this)" />
|
||||
</groupbox>
|
||||
|
||||
<groupbox id="manualVerification" hidden="true">
|
||||
<caption label="&authDialog.manualVerification;" />
|
||||
<description style="width: 300px; white-space: pre-wrap;">&authDialog.manualInstruction;</description>
|
||||
<html:textarea id="fingerprints" rows="5" readonly="true" />
|
||||
<hbox align="center">
|
||||
<label value="&authDialog.verified;" />
|
||||
<menulist id="verifiedOption">
|
||||
<menupopup>
|
||||
<menuitem label="&authDialog.yes;" value="yes" />
|
||||
<menuitem label="&authDialog.no;" value="no" />
|
||||
</menupopup>
|
||||
</menulist>
|
||||
</hbox>
|
||||
</groupbox>
|
||||
|
||||
<groupbox id="ask" hidden="true">
|
||||
<description id="askLabel" style="width: 300px; white-space: pre-wrap;" />
|
||||
<textbox id="response" oninput="otrAuth.oninput(this)" />
|
||||
</groupbox>
|
||||
|
||||
</dialog>
|
|
@ -0,0 +1,8 @@
|
|||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<!ENTITY state.label "Encryption Status:">
|
||||
<!ENTITY start.label "Start private conversation">
|
||||
<!ENTITY end.label "End private conversation">
|
||||
<!ENTITY auth.label "Verify your contact's identity">
|
|
@ -0,0 +1,6 @@
|
|||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<!ENTITY privDialog.title "Generating private key">
|
||||
<!ENTITY privDialog.done "Done">
|
|
@ -0,0 +1,29 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const {
|
||||
XPCOMUtils,
|
||||
l10nHelper,
|
||||
} = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
|
||||
const {OTR} = ChromeUtils.import("resource:///modules/OTR.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "_", () =>
|
||||
l10nHelper("chrome://chat/content/otr-generate-key.properties")
|
||||
);
|
||||
|
||||
var otrPriv = {
|
||||
|
||||
onload() {
|
||||
let args = window.arguments[0].wrappedJSObject;
|
||||
let priv = document.getElementById("priv");
|
||||
priv.textContent = _("priv.account", args.account, OTR.protocolName(args.protocol));
|
||||
OTR.generatePrivateKey(args.account, args.protocol).then(function() {
|
||||
document.documentElement.getButton("accept").disabled = false;
|
||||
document.documentElement.acceptDialog();
|
||||
}).catch(function(err) {
|
||||
document.documentElement.getButton("accept").disabled = false;
|
||||
priv.textContent = _("priv.failed", String(err));
|
||||
});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
# LOCALIZATION NOTE (priv.account): 1st %S is the name of the user's own chat account. 2nd %S is the chat communication protocol used by that account.
|
||||
priv.account=Generating private key for %S (%S) …
|
||||
# LOCALIZATION NOTE (priv.failed): %S contains an error message that describes the cause of the failure
|
||||
priv.failed=Generating key failed: %S
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" ?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css" ?>
|
||||
|
||||
<!DOCTYPE dialog [
|
||||
<!ENTITY % chatPrivDTD SYSTEM "chrome://chat/content/otr-generate-key.dtd">
|
||||
%chatPrivDTD;
|
||||
]>
|
||||
|
||||
<dialog
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
id="otrPrivDialog"
|
||||
windowtype="OTR:Priv"
|
||||
title="&privDialog.title;"
|
||||
buttons="accept"
|
||||
buttonlabelaccept="&privDialog.done;"
|
||||
onload="otrPriv.onload()"
|
||||
buttondisabledaccept="true">
|
||||
|
||||
<script type="application/javascript" src="chrome://chat/content/otr-generate-key.js" />
|
||||
<description id="priv" style="width: 300px; white-space: pre-wrap;"></description>
|
||||
|
||||
</dialog>
|
|
@ -0,0 +1,72 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
# LOCALIZATION NOTE (msgevent.encryption_required_part1): %S is the name of a chat contact
|
||||
msgevent.encryption_required_part1=You attempted to send an unencrypted message to %S. As a policy, unencrypted messages are not allowed.
|
||||
msgevent.encryption_required_part2=Attempting to start a private conversation. Your message will be retransmitted when the private conversation starts.
|
||||
msgevent.encryption_error=An error occurred when encrypting your message. The message was not sent.
|
||||
|
||||
# LOCALIZATION NOTE (msgevent.connection_ended): %S is the name of a chat contact
|
||||
msgevent.connection_ended=%S has already closed their private connection to you. Your message was not sent. Either end your private conversation, or restart it.
|
||||
|
||||
# LOCALIZATION NOTE (msgevent.setup_error): %S is the name of a chat contact
|
||||
msgevent.setup_error=An error occured while setting up a private conversation with %S.
|
||||
# LOCALIZATION NOTE (msgevent.msg_reflected): do not translate OTR which is the name of an encryption protocol
|
||||
msgevent.msg_reflected=You are receiving your own OTR messages. You are either trying to talk to yourself, or someone is reflecting your messages back at you.
|
||||
|
||||
# LOCALIZATION NOTE (msgevent.msg_resent): %S is the name of a chat contact
|
||||
msgevent.msg_resent=The last message to %S was resent.
|
||||
|
||||
# LOCALIZATION NOTE (msgevent.rcvdmsg_not_private): %S is the name of a chat contact
|
||||
msgevent.rcvdmsg_not_private=The encrypted message received from %S is unreadable, as you are not currently communicating privately.
|
||||
|
||||
# LOCALIZATION NOTE (msgevent.rcvdmsg_unreadable): %S is the name of a chat contact
|
||||
msgevent.rcvdmsg_unreadable=We received an unreadable encrypted message from %S.
|
||||
|
||||
# LOCALIZATION NOTE (msgevent.rcvdmsg_malformed): %S is the name of a chat contact
|
||||
msgevent.rcvdmsg_malformed=We received a malformed data message from %S.
|
||||
|
||||
# LOCALIZATION NOTE (msgevent.log_heartbeat_rcvd): %S is the name of a chat contact. A Heartbeat is a technical message used to keep a connection alive.
|
||||
msgevent.log_heartbeat_rcvd=Heartbeat received from %S.
|
||||
|
||||
# LOCALIZATION NOTE (msgevent.log_heartbeat_sent): %S is the name of a chat contact. A Heartbeat is a technical message used to keep a connection alive.
|
||||
msgevent.log_heartbeat_sent=Heartbeat sent to %S.
|
||||
|
||||
# LOCALIZATION NOTE (msgevent.rcvdmsg_general_err): do not translate OTR which is the name of an encryption protocol
|
||||
msgevent.rcvdmsg_general_err=An OTR error occured.
|
||||
|
||||
# LOCALIZATION NOTE (msgevent.rcvdmsg_unencrypted): 1st %S is the name of a chat contact. 2nd %S is the message that was received.
|
||||
msgevent.rcvdmsg_unencrypted=The following message received from %S was not encrypted: %S
|
||||
|
||||
# LOCALIZATION NOTE (msgevent.rcvdmsg_unrecognized): do not translate OTR which is the name of an encryption protocol. %S is the name of a chat contact.
|
||||
msgevent.rcvdmsg_unrecognized=We received an unrecognized OTR message from %S.
|
||||
|
||||
# LOCALIZATION NOTE (msgevent.rcvdmsg_for_other_instance): %S is the name of a chat contact
|
||||
msgevent.rcvdmsg_for_other_instance=%S has sent a message intended for a different session. If you are logged in multiple times, another session may have received the message.
|
||||
|
||||
# LOCALIZATION NOTE (context.gone_secure_private): %S is the name of a chat contact
|
||||
context.gone_secure_private=Private conversation with %S started.
|
||||
|
||||
# LOCALIZATION NOTE (context.gone_secure_unverified): %S is the name of a chat contact
|
||||
context.gone_secure_unverified=Private conversation with %S started. However, their identity has not been verified.
|
||||
|
||||
# LOCALIZATION NOTE (context.still_secure): %S is the name of a chat contact
|
||||
context.still_secure=Successfully refreshed the private conversation with %S.
|
||||
|
||||
error.enc=Error occurred encrypting message.
|
||||
|
||||
# LOCALIZATION NOTE (error.not_priv): %S is the name of a chat contact
|
||||
error.not_priv=You sent encrypted data to %S, who wasn't expecting it.
|
||||
error.unreadable=You transmitted an unreadable encrypted message.
|
||||
error.malformed=You transmitted a malformed data message.
|
||||
resent=[resent]
|
||||
# LOCALIZATION NOTE (tlv.disconnected): %S is the name of a chat contact
|
||||
tlv.disconnected=%S has ended their private conversation with you; you should do the same.
|
||||
# LOCALIZATION NOTE (query.msg): %S is the name of a chat contact. Do not translate "Off-the-Record" and "OTR" which is the name of an encryption protocol
|
||||
query.msg=%S has requested an Off-the-Record (OTR) private conversation. However, you do not have a plugin to support that. See https://en.wikipedia.org/wiki/Off-the-Record_Messaging for more information.
|
||||
trust.unused=Unused
|
||||
trust.not_private=Not Private
|
||||
trust.unverified=Unverified
|
||||
trust.private=Private
|
||||
trust.finished=Finished
|
|
@ -0,0 +1,59 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
start.label=Start private conversation
|
||||
refresh.label=Refresh private conversation
|
||||
auth.label=Verify your contact's identity
|
||||
auth.cancel=Cancel
|
||||
auth.cancelAccessKey=C
|
||||
auth.error=An error occurred while verifying your contact's identity.
|
||||
auth.success=Verifying your contact's identity completed successfully.
|
||||
auth.successThem=Your contact has successfully verified your identity. You may want to verify their identity as well by asking your own question.
|
||||
auth.fail=Failed to verify your contact's identity.
|
||||
auth.waiting=Waiting for contact to complete verification …
|
||||
reauth.label=Reverify your contact's identity
|
||||
finger.verify=Verify
|
||||
verify.accessKey=V
|
||||
|
||||
# LOCALIZATION NOTE (buddycontextmenu.label): do not translate OTR which is the name of an encryption protocol
|
||||
buddycontextmenu.label=Add Contact's OTR Fingerprint
|
||||
|
||||
# LOCALIZATION NOTE (alert.start): %S is the name of a chat contact
|
||||
alert.start=Attempting to start a private conversation with %S.
|
||||
# LOCALIZATION NOTE (alert.refresh): %S is the name of a chat contact
|
||||
alert.refresh=Attempting to refresh the private conversation with %S.
|
||||
# LOCALIZATION NOTE (alert.gone_insecure): %S is the name of a chat contact
|
||||
alert.gone_insecure=Private conversation with %S ended.
|
||||
|
||||
# LOCALIZATION NOTE (finger.unseen): %S is the name of a chat contact
|
||||
finger.unseen=The identity of %S has not been verified yet. Casual eavesdropping is not possible, but with some effort someone could be listening in. You should verify this contact's identity.
|
||||
|
||||
state.not_private=The current conversation is not private.
|
||||
|
||||
# LOCALIZATION NOTE (state.unverified): %S is the name of a chat contact
|
||||
state.unverified=The current conversation is private but the identity of %S has not been verified.
|
||||
|
||||
# LOCALIZATION NOTE (state.private): %S is the name of a chat contact
|
||||
state.private=The current conversation is private and the identity of %S has been verified.
|
||||
|
||||
# LOCALIZATION NOTE (state.finished): %S is the name of a chat contact
|
||||
state.finished=%S has ended their private conversation with you; you should do the same.
|
||||
|
||||
state.not_private.label=Insecure
|
||||
state.unverified.label=Unverified
|
||||
state.private.label=Private
|
||||
state.finished.label=Finished
|
||||
|
||||
# LOCALIZATION NOTE (afterauth.private): %S is the name of a chat contact
|
||||
afterauth.private=You have verified the identity of %S.
|
||||
|
||||
# LOCALIZATION NOTE (afterauth.unverified): %S is the name of a chat contact
|
||||
afterauth.unverified=The identity of %S has not been verified.
|
||||
|
||||
verify.title=Verify your contact's identity
|
||||
error.title=Error
|
||||
success.title=End to End Encryption
|
||||
successThem.title=Verify your contact's identity
|
||||
fail.title=Unable to verify
|
||||
waiting.title=Verification request sent
|
|
@ -0,0 +1,54 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* eslint-env mozilla/chrome-worker, node */
|
||||
importScripts("resource://gre/modules/workers/require.js");
|
||||
var PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js");
|
||||
var Funcs = {};
|
||||
|
||||
// Only what we need from libotr.js
|
||||
Funcs.generateKey = function(path, otrl_version, newkeySource) {
|
||||
// eslint-disable-next-line no-eval
|
||||
let newkey = eval(newkeySource); // jshint ignore:line
|
||||
let libotr = ctypes.open(path);
|
||||
|
||||
let abi = ctypes.default_abi;
|
||||
let gcry_error_t = ctypes.unsigned_int;
|
||||
|
||||
// Initialize the OTR library. Pass the version of the API you are using.
|
||||
let otrl_init = libotr.declare(
|
||||
"otrl_init", abi, gcry_error_t,
|
||||
ctypes.unsigned_int, ctypes.unsigned_int, ctypes.unsigned_int
|
||||
);
|
||||
|
||||
// Do the private key generation calculation. You may call this from a
|
||||
// background thread. When it completes, call
|
||||
// otrl_privkey_generate_finish from the _main_ thread.
|
||||
let otrl_privkey_generate_calculate = libotr.declare(
|
||||
"otrl_privkey_generate_calculate", abi, gcry_error_t,
|
||||
ctypes.void_t.ptr
|
||||
);
|
||||
|
||||
otrl_init.apply(libotr, otrl_version);
|
||||
let err = otrl_privkey_generate_calculate(newkey);
|
||||
libotr.close();
|
||||
if (err)
|
||||
throw new Error("otrl_privkey_generate_calculate (" + err + ")");
|
||||
};
|
||||
|
||||
var worker = new PromiseWorker.AbstractWorker();
|
||||
|
||||
worker.dispatch = function(method, args = []) {
|
||||
return Funcs[method](...args);
|
||||
};
|
||||
|
||||
worker.postMessage = function(res, ...args) {
|
||||
self.postMessage(res, ...args);
|
||||
};
|
||||
|
||||
worker.close = function() {
|
||||
self.close();
|
||||
};
|
||||
|
||||
self.addEventListener("message", msg => worker.handleMessage(msg));
|
|
@ -22,3 +22,11 @@
|
|||
locale/@AB_CD@/chat/twitter.properties (%twitter.properties)
|
||||
locale/@AB_CD@/chat/xmpp.properties (%xmpp.properties)
|
||||
locale/@AB_CD@/chat/yahoo.properties (%yahoo.properties)
|
||||
# locale/@AB_CD@/chat/otr-auth.dtd (%otr-auth.dtd)
|
||||
# locale/@AB_CD@/chat/otr-auth.properties (%otr-auth.properties)
|
||||
# locale/@AB_CD@/chat/otr-add-finger.dtd (%otr-add-finger.dtd)
|
||||
# locale/@AB_CD@/chat/otr-add-finger.properties (%otr-add-finger.properties)
|
||||
# locale/@AB_CD@/chat/otr.properties (%otr.properties)
|
||||
# locale/@AB_CD@/chat/otr-generate-key.dtd (%otr-generate-key.dtd)
|
||||
# locale/@AB_CD@/chat/otr-generate-key.properties (%otr-generate-key.properties)
|
||||
# locale/@AB_CD@/chat/otrUI.properties (%otrUI.properties)
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const {ctypes} = ChromeUtils.import("resource://gre/modules/ctypes.jsm");
|
||||
const {Services} = ChromeUtils.import("resource:///modules/imServices.jsm");
|
||||
|
||||
var OS = Services.appinfo.OS.toLowerCase();
|
||||
|
||||
// type defs
|
||||
|
||||
var FILE = ctypes.StructType("FILE");
|
||||
var fname_t = ctypes.char.ptr;
|
||||
var wchar_t = ctypes.char16_t;
|
||||
|
||||
// Set the abi and path to CLib based on the OS.
|
||||
var libcAbi, libcPath;
|
||||
var strdup = "strdup";
|
||||
var fopen = "fopen";
|
||||
|
||||
switch (OS) {
|
||||
case "win32":
|
||||
case "winnt":
|
||||
libcAbi = ctypes.winapi_abi;
|
||||
libcPath = ctypes.libraryName("msvcrt");
|
||||
strdup = "_strdup";
|
||||
fopen = "_wfopen";
|
||||
fname_t = wchar_t.ptr;
|
||||
break;
|
||||
case "darwin":
|
||||
libcAbi = ctypes.default_abi;
|
||||
libcPath = ctypes.libraryName("c");
|
||||
break;
|
||||
case "linux":
|
||||
libcAbi = ctypes.default_abi;
|
||||
libcPath = "libc.so.6";
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unknown OS");
|
||||
}
|
||||
|
||||
var libc = ctypes.open(libcPath);
|
||||
|
||||
var CLib = {
|
||||
FILE,
|
||||
memcmp: libc.declare(
|
||||
"memcmp", libcAbi, ctypes.int,
|
||||
ctypes.void_t.ptr,
|
||||
ctypes.void_t.ptr,
|
||||
ctypes.size_t
|
||||
),
|
||||
free: libc.declare(
|
||||
"free", libcAbi, ctypes.void_t,
|
||||
ctypes.void_t.ptr
|
||||
),
|
||||
strdup: libc.declare(
|
||||
strdup, libcAbi, ctypes.char.ptr,
|
||||
ctypes.char.ptr
|
||||
),
|
||||
fclose: libc.declare(
|
||||
"fclose", libcAbi, ctypes.int,
|
||||
FILE.ptr
|
||||
),
|
||||
fopen: libc.declare(
|
||||
fopen, libcAbi, FILE.ptr,
|
||||
fname_t,
|
||||
fname_t
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
// exports
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["CLib"];
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,38 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm");
|
||||
const {Services} = ChromeUtils.import("resource:///modules/imServices.jsm");
|
||||
|
||||
var OTRHelpers = {
|
||||
|
||||
profilePath(filename) {
|
||||
return OS.Path.join(OS.Constants.Path.profileDir, filename);
|
||||
},
|
||||
|
||||
* getAccounts() {
|
||||
let accounts = Services.accounts.getAccounts();
|
||||
while (accounts.hasMoreElements())
|
||||
yield accounts.getNext();
|
||||
},
|
||||
|
||||
readTextFile(filename) {
|
||||
let decoder = new TextDecoder();
|
||||
return OS.File.read(filename).then(function(array) {
|
||||
return decoder.decode(array);
|
||||
});
|
||||
},
|
||||
|
||||
writeTextFile(filename, data) {
|
||||
let encoder = new TextEncoder();
|
||||
let array = encoder.encode(data);
|
||||
// https://dutherenverseauborddelatable.wordpress.com/2014/02/05/is-my-data-on-the-disk-safety-properties-of-os-file-writeatomic/
|
||||
return OS.File.writeAtomic(filename, array, { tmpPath: `${filename}.tmp` });
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
// exports
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["OTRHelpers"];
|
|
@ -0,0 +1,918 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const otrl_version = [4, 1, 1];
|
||||
|
||||
const {CLib} = ChromeUtils.import("resource:///modules/CLib.jsm");
|
||||
const {ctypes} = ChromeUtils.import("resource://gre/modules/ctypes.jsm");
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
var systemOS = Services.appinfo.OS.toLowerCase();
|
||||
|
||||
const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm");
|
||||
|
||||
var abi = ctypes.default_abi;
|
||||
|
||||
// Open libotr. Determine the path to the chrome directory and look for it
|
||||
// there first. If not, fallback to searching the standard locations.
|
||||
var libotr, libotrPath;
|
||||
|
||||
function tryLoadOTR(name, suffix) {
|
||||
let filename = ctypes.libraryName(name) + suffix;
|
||||
let binPath = Services.dirsvc.get("XpcomLib", Ci.nsIFile).path;
|
||||
let binDir = OS.Path.dirname(binPath);
|
||||
libotrPath = OS.Path.join(binDir, filename);
|
||||
|
||||
try {
|
||||
console.log("===> trying to load " + libotrPath);
|
||||
libotr = ctypes.open(libotrPath);
|
||||
} catch (e) {}
|
||||
|
||||
if (!libotr) {
|
||||
try {
|
||||
// look in standard locations
|
||||
libotrPath = filename;
|
||||
console.log("===> trying to load " +
|
||||
libotrPath + " from system's standard locations");
|
||||
libotr = ctypes.open(libotrPath);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (!libotr && (systemOS === "winnt" || systemOS === "darwin")) {
|
||||
// otr.5.dll or otr.5.dylib
|
||||
tryLoadOTR("otr.5", "");
|
||||
}
|
||||
|
||||
if (!libotr && systemOS === "winnt") {
|
||||
// otr-5.dll
|
||||
tryLoadOTR("otr-5", "");
|
||||
}
|
||||
|
||||
if (!libotr && systemOS === "winnt") {
|
||||
// libotr-5.dll
|
||||
tryLoadOTR("libotr-5", "");
|
||||
}
|
||||
|
||||
if (!libotr && !(systemOS === "winnt") && !(systemOS === "darwin")) {
|
||||
// libotr.so.5
|
||||
tryLoadOTR("otr", ".5");
|
||||
}
|
||||
|
||||
if (!libotr) {
|
||||
tryLoadOTR("otr", "");
|
||||
}
|
||||
|
||||
// Helper function to open files with the path properly encoded.
|
||||
var callWithFILEp = function() {
|
||||
// Windows filenames are in UTF-16.
|
||||
let charType = (systemOS === "winnt") ? "jschar" : "char";
|
||||
|
||||
let args = Array.from(arguments);
|
||||
let func = args.shift() + "_FILEp";
|
||||
let mode = ctypes[charType].array()(args.shift());
|
||||
let ind = args.shift();
|
||||
let filename = ctypes[charType].array()(args[ind]);
|
||||
|
||||
let file = CLib.fopen(filename, mode);
|
||||
if (file.isNull())
|
||||
return 1;
|
||||
|
||||
// Swap filename with file.
|
||||
args[ind] = file;
|
||||
|
||||
let ret = OTRLib[func].apply(OTRLib, args);
|
||||
CLib.fclose(file);
|
||||
return ret;
|
||||
};
|
||||
|
||||
// type defs
|
||||
|
||||
const FILE = CLib.FILE;
|
||||
|
||||
const time_t = ctypes.long;
|
||||
const gcry_error_t = ctypes.unsigned_int;
|
||||
const gcry_cipher_hd_t = ctypes.StructType("gcry_cipher_handle").ptr;
|
||||
const gcry_md_hd_t = ctypes.StructType("gcry_md_handle").ptr;
|
||||
const gcry_mpi_t = ctypes.StructType("gcry_mpi").ptr;
|
||||
|
||||
const otrl_instag_t = ctypes.unsigned_int;
|
||||
const OtrlPolicy = ctypes.unsigned_int;
|
||||
const OtrlTLV = ctypes.StructType("s_OtrlTLV");
|
||||
const ConnContext = ctypes.StructType("context");
|
||||
const ConnContextPriv = ctypes.StructType("context_priv");
|
||||
const OtrlMessageAppOps = ctypes.StructType("s_OtrlMessageAppOps");
|
||||
const OtrlAuthInfo = ctypes.StructType("OtrlAuthInfo");
|
||||
const Fingerprint = ctypes.StructType("s_fingerprint");
|
||||
const s_OtrlUserState = ctypes.StructType("s_OtrlUserState");
|
||||
const OtrlUserState = s_OtrlUserState.ptr;
|
||||
const OtrlSMState = ctypes.StructType("OtrlSMState");
|
||||
const DH_keypair = ctypes.StructType("DH_keypair");
|
||||
const OtrlPrivKey = ctypes.StructType("s_OtrlPrivKey");
|
||||
const OtrlInsTag = ctypes.StructType("s_OtrlInsTag");
|
||||
const OtrlPendingPrivKey = ctypes.StructType("s_OtrlPendingPrivKey");
|
||||
|
||||
const OTRL_PRIVKEY_FPRINT_HUMAN_LEN = 45;
|
||||
const fingerprint_t = ctypes.char.array(OTRL_PRIVKEY_FPRINT_HUMAN_LEN);
|
||||
const hash_t = ctypes.unsigned_char.array(20);
|
||||
|
||||
const app_data_free_t = ctypes.FunctionType(abi, ctypes.void_t, [
|
||||
ctypes.void_t.ptr,
|
||||
]).ptr;
|
||||
|
||||
// enums
|
||||
|
||||
const OtrlErrorCode = ctypes.int;
|
||||
const OtrlSMPEvent = ctypes.int;
|
||||
const OtrlMessageEvent = ctypes.int;
|
||||
const OtrlFragmentPolicy = ctypes.int;
|
||||
const OtrlConvertType = ctypes.int;
|
||||
const OtrlMessageState = ctypes.int;
|
||||
const OtrlAuthState = ctypes.int;
|
||||
const OtrlSessionIdHalf = ctypes.int;
|
||||
const OtrlSMProgState = ctypes.int;
|
||||
const NextExpectedSMP = ctypes.int;
|
||||
|
||||
// callback signatures
|
||||
|
||||
const policy_cb_t = ctypes.FunctionType(abi, OtrlPolicy, [
|
||||
ctypes.void_t.ptr, ConnContext.ptr,
|
||||
]).ptr;
|
||||
|
||||
const create_privkey_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
|
||||
ctypes.void_t.ptr, ctypes.char.ptr, ctypes.char.ptr,
|
||||
]).ptr;
|
||||
|
||||
const is_logged_in_cb_t = ctypes.FunctionType(abi, ctypes.int, [
|
||||
ctypes.void_t.ptr, ctypes.char.ptr, ctypes.char.ptr, ctypes.char.ptr,
|
||||
]).ptr;
|
||||
|
||||
const inject_message_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
|
||||
ctypes.void_t.ptr, ctypes.char.ptr, ctypes.char.ptr, ctypes.char.ptr,
|
||||
ctypes.char.ptr,
|
||||
]).ptr;
|
||||
|
||||
const update_context_list_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
|
||||
ctypes.void_t.ptr,
|
||||
]).ptr;
|
||||
|
||||
const new_fingerprint_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
|
||||
ctypes.void_t.ptr, OtrlUserState, ctypes.char.ptr, ctypes.char.ptr,
|
||||
ctypes.char.ptr, ctypes.unsigned_char.array(20),
|
||||
]).ptr;
|
||||
|
||||
const write_fingerprint_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
|
||||
ctypes.void_t.ptr,
|
||||
]).ptr;
|
||||
|
||||
const gone_secure_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
|
||||
ctypes.void_t.ptr, ConnContext.ptr,
|
||||
]).ptr;
|
||||
|
||||
const gone_insecure_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
|
||||
ctypes.void_t.ptr, ConnContext.ptr,
|
||||
]).ptr;
|
||||
|
||||
const still_secure_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
|
||||
ctypes.void_t.ptr, ConnContext.ptr, ctypes.int,
|
||||
]).ptr;
|
||||
|
||||
const max_message_size_cb_t = ctypes.FunctionType(abi, ctypes.int, [
|
||||
ctypes.void_t.ptr, ConnContext.ptr,
|
||||
]).ptr;
|
||||
|
||||
const account_name_cb_t = ctypes.FunctionType(abi, ctypes.char.ptr, [
|
||||
ctypes.void_t.ptr, ctypes.char.ptr, ctypes.char.ptr,
|
||||
]).ptr;
|
||||
|
||||
const account_name_free_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
|
||||
ctypes.void_t.ptr, ctypes.char.ptr,
|
||||
]).ptr;
|
||||
|
||||
const received_symkey_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
|
||||
ctypes.void_t.ptr, ConnContext.ptr, ctypes.unsigned_int,
|
||||
ctypes.unsigned_char.ptr, ctypes.size_t, ctypes.unsigned_char.ptr,
|
||||
]).ptr;
|
||||
|
||||
const otr_error_message_cb_t = ctypes.FunctionType(abi, ctypes.char.ptr, [
|
||||
ctypes.void_t.ptr, ConnContext.ptr, OtrlErrorCode,
|
||||
]).ptr;
|
||||
|
||||
const otr_error_message_free_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
|
||||
ctypes.void_t.ptr, ctypes.char.ptr,
|
||||
]).ptr;
|
||||
|
||||
const resent_msg_prefix_cb_t = ctypes.FunctionType(abi, ctypes.char.ptr, [
|
||||
ctypes.void_t.ptr, ConnContext.ptr,
|
||||
]).ptr;
|
||||
|
||||
const resent_msg_prefix_free_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
|
||||
ctypes.void_t.ptr, ctypes.char.ptr,
|
||||
]).ptr;
|
||||
|
||||
const handle_smp_event_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
|
||||
ctypes.void_t.ptr, OtrlSMPEvent, ConnContext.ptr, ctypes.unsigned_short,
|
||||
ctypes.char.ptr,
|
||||
]).ptr;
|
||||
|
||||
const handle_msg_event_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
|
||||
ctypes.void_t.ptr, OtrlMessageEvent, ConnContext.ptr, ctypes.char.ptr,
|
||||
gcry_error_t,
|
||||
]).ptr;
|
||||
|
||||
const create_instag_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
|
||||
ctypes.void_t.ptr, ctypes.char.ptr, ctypes.char.ptr,
|
||||
]).ptr;
|
||||
|
||||
const convert_msg_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
|
||||
ctypes.void_t.ptr, ConnContext.ptr, OtrlConvertType, ctypes.char.ptr.ptr,
|
||||
ctypes.char.ptr,
|
||||
]).ptr;
|
||||
|
||||
const convert_free_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
|
||||
ctypes.void_t.ptr, ConnContext.ptr, ctypes.char.ptr,
|
||||
]).ptr;
|
||||
|
||||
const timer_control_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
|
||||
ctypes.void_t.ptr, ctypes.unsigned_int,
|
||||
]).ptr;
|
||||
|
||||
// defines
|
||||
|
||||
s_OtrlUserState.define([
|
||||
{ context_root: ConnContext.ptr },
|
||||
{ privkey_root: OtrlPrivKey.ptr },
|
||||
{ instag_root: OtrlInsTag.ptr },
|
||||
{ pending_root: OtrlPendingPrivKey.ptr },
|
||||
{ timer_running: ctypes.int },
|
||||
]);
|
||||
|
||||
Fingerprint.define([
|
||||
{ next: Fingerprint.ptr },
|
||||
{ tous: Fingerprint.ptr.ptr },
|
||||
{ fingerprint: ctypes.unsigned_char.ptr },
|
||||
{ context: ConnContext.ptr },
|
||||
{ trust: ctypes.char.ptr },
|
||||
]);
|
||||
|
||||
DH_keypair.define([
|
||||
{ groupid: ctypes.unsigned_int },
|
||||
{ priv: gcry_mpi_t },
|
||||
{ pub: gcry_mpi_t },
|
||||
]);
|
||||
|
||||
OtrlSMState.define([
|
||||
{ secret: gcry_mpi_t },
|
||||
{ x2: gcry_mpi_t },
|
||||
{ x3: gcry_mpi_t },
|
||||
{ g1: gcry_mpi_t },
|
||||
{ g2: gcry_mpi_t },
|
||||
{ g3: gcry_mpi_t },
|
||||
{ g3o: gcry_mpi_t },
|
||||
{ p: gcry_mpi_t },
|
||||
{ q: gcry_mpi_t },
|
||||
{ pab: gcry_mpi_t },
|
||||
{ qab: gcry_mpi_t },
|
||||
{ nextExpected: NextExpectedSMP },
|
||||
{ received_question: ctypes.int },
|
||||
{ sm_prog_state: OtrlSMProgState },
|
||||
]);
|
||||
|
||||
OtrlAuthInfo.define([
|
||||
{ authstate: OtrlAuthState },
|
||||
{ context: ConnContext.ptr },
|
||||
{ our_dh: DH_keypair },
|
||||
{ our_keyid: ctypes.unsigned_int },
|
||||
{ encgx: ctypes.unsigned_char.ptr },
|
||||
{ encgx_len: ctypes.size_t },
|
||||
{ r: ctypes.unsigned_char.array(16) },
|
||||
{ hashgx: ctypes.unsigned_char.array(32) },
|
||||
{ their_pub: gcry_mpi_t },
|
||||
{ their_keyid: ctypes.unsigned_int },
|
||||
{ enc_c: gcry_cipher_hd_t },
|
||||
{ enc_cp: gcry_cipher_hd_t },
|
||||
{ mac_m1: gcry_md_hd_t },
|
||||
{ mac_m1p: gcry_md_hd_t },
|
||||
{ mac_m2: gcry_md_hd_t },
|
||||
{ mac_m2p: gcry_md_hd_t },
|
||||
{ their_fingerprint: ctypes.unsigned_char.array(20) },
|
||||
{ initiated: ctypes.int },
|
||||
{ protocol_version: ctypes.unsigned_int },
|
||||
{ secure_session_id: ctypes.unsigned_char.array(20) },
|
||||
{ secure_session_id_len: ctypes.size_t },
|
||||
{ session_id_half: OtrlSessionIdHalf },
|
||||
{ lastauthmsg: ctypes.char.ptr },
|
||||
{ commit_sent_time: time_t },
|
||||
]);
|
||||
|
||||
ConnContext.define([
|
||||
{ next: ConnContext.ptr },
|
||||
{ tous: ConnContext.ptr.ptr },
|
||||
{ context_priv: ConnContextPriv.ptr },
|
||||
{ username: ctypes.char.ptr },
|
||||
{ accountname: ctypes.char.ptr },
|
||||
{ protocol: ctypes.char.ptr },
|
||||
{ m_context: ConnContext.ptr },
|
||||
{ recent_rcvd_child: ConnContext.ptr },
|
||||
{ recent_sent_child: ConnContext.ptr },
|
||||
{ recent_child: ConnContext.ptr },
|
||||
{ our_instance: otrl_instag_t },
|
||||
{ their_instance: otrl_instag_t },
|
||||
{ msgstate: OtrlMessageState },
|
||||
{ auth: OtrlAuthInfo },
|
||||
{ fingerprint_root: Fingerprint },
|
||||
{ active_fingerprint: Fingerprint.ptr },
|
||||
{ sessionid: ctypes.unsigned_char.array(20) },
|
||||
{ sessionid_len: ctypes.size_t },
|
||||
{ sessionid_half: OtrlSessionIdHalf },
|
||||
{ protocol_version: ctypes.unsigned_int },
|
||||
{ otr_offer: ctypes.int },
|
||||
{ app_data: ctypes.void_t.ptr },
|
||||
{ app_data_free: app_data_free_t },
|
||||
{ smstate: OtrlSMState.ptr },
|
||||
]);
|
||||
|
||||
OtrlMessageAppOps.define([
|
||||
{ policy: policy_cb_t },
|
||||
{ create_privkey: create_privkey_cb_t },
|
||||
{ is_logged_in: is_logged_in_cb_t },
|
||||
{ inject_message: inject_message_cb_t },
|
||||
{ update_context_list: update_context_list_cb_t },
|
||||
{ new_fingerprint: new_fingerprint_cb_t },
|
||||
{ write_fingerprint: write_fingerprint_cb_t },
|
||||
{ gone_secure: gone_secure_cb_t },
|
||||
{ gone_insecure: gone_insecure_cb_t },
|
||||
{ still_secure: still_secure_cb_t },
|
||||
{ max_message_size: max_message_size_cb_t },
|
||||
{ account_name: account_name_cb_t },
|
||||
{ account_name_free: account_name_free_cb_t },
|
||||
{ received_symkey: received_symkey_cb_t },
|
||||
{ otr_error_message: otr_error_message_cb_t },
|
||||
{ otr_error_message_free: otr_error_message_free_cb_t },
|
||||
{ resent_msg_prefix: resent_msg_prefix_cb_t },
|
||||
{ resent_msg_prefix_free: resent_msg_prefix_free_cb_t },
|
||||
{ handle_smp_event: handle_smp_event_cb_t },
|
||||
{ handle_msg_event: handle_msg_event_cb_t },
|
||||
{ create_instag: create_instag_cb_t },
|
||||
{ convert_msg: convert_msg_cb_t },
|
||||
{ convert_free: convert_free_cb_t },
|
||||
{ timer_control: timer_control_cb_t },
|
||||
]);
|
||||
|
||||
OtrlTLV.define([
|
||||
{ type: ctypes.unsigned_short },
|
||||
{ len: ctypes.unsigned_short },
|
||||
{ data: ctypes.unsigned_char.ptr },
|
||||
{ next: OtrlTLV.ptr },
|
||||
]);
|
||||
|
||||
// policies
|
||||
|
||||
// const OTRL_POLICY_ALLOW_V1 = 0x01;
|
||||
const OTRL_POLICY_ALLOW_V2 = 0x02;
|
||||
|
||||
// const OTRL_POLICY_ALLOW_V3 = 0x04;
|
||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1550474 re v3.
|
||||
|
||||
const OTRL_POLICY_REQUIRE_ENCRYPTION = 0x08;
|
||||
const OTRL_POLICY_SEND_WHITESPACE_TAG = 0x10;
|
||||
const OTRL_POLICY_WHITESPACE_START_AKE = 0x20;
|
||||
|
||||
// const OTRL_POLICY_ERROR_START_AKE = 0x40;
|
||||
// Disabled to avoid automatic resend and MITM, as explained in
|
||||
// https://github.com/arlolra/ctypes-otr/issues/55
|
||||
|
||||
var OTRLib;
|
||||
|
||||
if (libotr) OTRLib = {
|
||||
|
||||
path: libotrPath,
|
||||
|
||||
// libotr API version
|
||||
otrl_version,
|
||||
|
||||
init() {
|
||||
// console.log("===> OTRLib.init()\n");
|
||||
// apply version array as arguments to the init function
|
||||
if (this.otrl_init.apply(this, this.otrl_version)) {
|
||||
throw new Error("Couldn't initialize libotr.");
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
// proto.h
|
||||
|
||||
// If we ever see this sequence in a plaintext message, we'll assume the
|
||||
// other side speaks OTR, and try to establish a connection.
|
||||
OTRL_MESSAGE_TAG_BASE: " \t \t\t\t\t \t \t \t ",
|
||||
|
||||
OTRL_POLICY_OPPORTUNISTIC: new ctypes.unsigned_int(
|
||||
OTRL_POLICY_ALLOW_V2 |
|
||||
// OTRL_POLICY_ALLOW_V3 |
|
||||
OTRL_POLICY_SEND_WHITESPACE_TAG |
|
||||
OTRL_POLICY_WHITESPACE_START_AKE |
|
||||
// OTRL_POLICY_ERROR_START_AKE |
|
||||
0
|
||||
),
|
||||
|
||||
OTRL_POLICY_ALWAYS: new ctypes.unsigned_int(
|
||||
OTRL_POLICY_ALLOW_V2 |
|
||||
// OTRL_POLICY_ALLOW_V3 |
|
||||
OTRL_POLICY_REQUIRE_ENCRYPTION |
|
||||
OTRL_POLICY_WHITESPACE_START_AKE |
|
||||
// OTRL_POLICY_ERROR_START_AKE |
|
||||
0
|
||||
),
|
||||
|
||||
fragPolicy: {
|
||||
OTRL_FRAGMENT_SEND_SKIP: 0,
|
||||
OTRL_FRAGMENT_SEND_ALL: 1,
|
||||
OTRL_FRAGMENT_SEND_ALL_BUT_FIRST: 2,
|
||||
OTRL_FRAGMENT_SEND_ALL_BUT_LAST: 3,
|
||||
},
|
||||
|
||||
// Return a pointer to a newly-allocated OTR query message, customized
|
||||
// with our name. The caller should free() the result when he's done
|
||||
// with it.
|
||||
otrl_proto_default_query_msg: libotr.declare(
|
||||
"otrl_proto_default_query_msg", abi, ctypes.char.ptr,
|
||||
ctypes.char.ptr, OtrlPolicy
|
||||
),
|
||||
|
||||
// Initialize the OTR library. Pass the version of the API you are using.
|
||||
otrl_init: libotr.declare(
|
||||
"otrl_init", abi, gcry_error_t,
|
||||
ctypes.unsigned_int, ctypes.unsigned_int, ctypes.unsigned_int
|
||||
),
|
||||
|
||||
// instag.h
|
||||
|
||||
instag: {
|
||||
OTRL_INSTAG_MASTER: new ctypes.unsigned_int(0),
|
||||
OTRL_INSTAG_BEST: new ctypes.unsigned_int(1),
|
||||
OTRL_INSTAG_RECENT: new ctypes.unsigned_int(2),
|
||||
OTRL_INSTAG_RECENT_RECEIVED: new ctypes.unsigned_int(3),
|
||||
OTRL_INSTAG_RECENT_SENT: new ctypes.unsigned_int(4),
|
||||
OTRL_MIN_VALID_INSTAG: new ctypes.unsigned_int(0x100),
|
||||
},
|
||||
|
||||
// Get a new instance tag for the given account and write to file. The FILE*
|
||||
// must be open for writing.
|
||||
otrl_instag_generate: callWithFILEp.bind(null, "otrl_instag_generate", "wb", 1),
|
||||
otrl_instag_generate_FILEp: libotr.declare(
|
||||
"otrl_instag_generate_FILEp", abi, gcry_error_t,
|
||||
OtrlUserState, FILE.ptr, ctypes.char.ptr, ctypes.char.ptr
|
||||
),
|
||||
|
||||
// Read our instance tag from a file on disk into the given OtrlUserState.
|
||||
// The FILE* must be open for reading.
|
||||
otrl_instag_read: callWithFILEp.bind(null, "otrl_instag_read", "rb", 1),
|
||||
otrl_instag_read_FILEp: libotr.declare(
|
||||
"otrl_instag_read_FILEp", abi, gcry_error_t,
|
||||
OtrlUserState, FILE.ptr
|
||||
),
|
||||
|
||||
// Write our instance tags to a file on disk. The FILE* must be open for
|
||||
// writing.
|
||||
otrl_instag_write: callWithFILEp.bind(null, "otrl_instag_write", "wb", 1),
|
||||
otrl_instag_write_FILEp: libotr.declare(
|
||||
"otrl_instag_write_FILEp", abi, gcry_error_t,
|
||||
OtrlUserState, FILE.ptr
|
||||
),
|
||||
|
||||
// auth.h
|
||||
|
||||
authState: {
|
||||
OTRL_AUTHSTATE_NONE: 0,
|
||||
OTRL_AUTHSTATE_AWAITING_DHKEY: 1,
|
||||
OTRL_AUTHSTATE_AWAITING_REVEALSIG: 2,
|
||||
OTRL_AUTHSTATE_AWAITING_SIG: 3,
|
||||
OTRL_AUTHSTATE_V1_SETUP: 4,
|
||||
},
|
||||
|
||||
// b64.h
|
||||
|
||||
// base64 encode data. Insert no linebreaks or whitespace.
|
||||
// The buffer base64data must contain at least ((datalen+2)/3)*4 bytes of
|
||||
// space. This function will return the number of bytes actually used.
|
||||
otrl_base64_encode: libotr.declare(
|
||||
"otrl_base64_encode", abi, ctypes.size_t,
|
||||
ctypes.char.ptr, ctypes.unsigned_char.ptr, ctypes.size_t
|
||||
),
|
||||
|
||||
// base64 decode data. Skip non-base64 chars, and terminate at the
|
||||
// first '=', or the end of the buffer.
|
||||
// The buffer data must contain at least ((base64len+3) / 4) * 3 bytes
|
||||
// of space. This function will return the number of bytes actually
|
||||
// used.
|
||||
otrl_base64_decode: libotr.declare(
|
||||
"otrl_base64_decode", abi, ctypes.size_t,
|
||||
ctypes.unsigned_char.ptr, ctypes.char.ptr, ctypes.size_t
|
||||
),
|
||||
|
||||
// context.h
|
||||
|
||||
otr_offer: {
|
||||
OFFER_NOT: 0,
|
||||
OFFER_SENT: 1,
|
||||
OFFER_REJECTED: 2,
|
||||
OFFER_ACCEPTED: 3,
|
||||
},
|
||||
|
||||
messageState: {
|
||||
OTRL_MSGSTATE_PLAINTEXT: 0,
|
||||
OTRL_MSGSTATE_ENCRYPTED: 1,
|
||||
OTRL_MSGSTATE_FINISHED: 2,
|
||||
},
|
||||
|
||||
// Look up a connection context by name/account/protocol/instance from the
|
||||
// given OtrlUserState.
|
||||
otrl_context_find: libotr.declare(
|
||||
"otrl_context_find", abi, ConnContext.ptr,
|
||||
OtrlUserState,
|
||||
ctypes.char.ptr,
|
||||
ctypes.char.ptr,
|
||||
ctypes.char.ptr,
|
||||
otrl_instag_t,
|
||||
ctypes.int,
|
||||
ctypes.int.ptr,
|
||||
ctypes.void_t.ptr,
|
||||
ctypes.void_t.ptr
|
||||
),
|
||||
|
||||
// Set the trust level for a given fingerprint.
|
||||
otrl_context_set_trust: libotr.declare(
|
||||
"otrl_context_set_trust", abi, ctypes.void_t,
|
||||
Fingerprint.ptr, ctypes.char.ptr
|
||||
),
|
||||
|
||||
// Find a fingerprint in a given context, perhaps adding it if not present.
|
||||
otrl_context_find_fingerprint: libotr.declare(
|
||||
"otrl_context_find_fingerprint", abi, Fingerprint.ptr,
|
||||
ConnContext.ptr, hash_t, ctypes.int, ctypes.int.ptr
|
||||
),
|
||||
|
||||
// Forget a fingerprint (and maybe the whole context).
|
||||
otrl_context_forget_fingerprint: libotr.declare(
|
||||
"otrl_context_forget_fingerprint", abi, ctypes.void_t,
|
||||
Fingerprint.ptr, ctypes.int
|
||||
),
|
||||
|
||||
// Return true iff the given fingerprint is marked as trusted.
|
||||
otrl_context_is_fingerprint_trusted: libotr.declare(
|
||||
"otrl_context_is_fingerprint_trusted", abi, ctypes.int,
|
||||
Fingerprint.ptr
|
||||
),
|
||||
|
||||
// dh.h
|
||||
|
||||
sessionIdHalf: {
|
||||
OTRL_SESSIONID_FIRST_HALF_BOLD: 0,
|
||||
OTRL_SESSIONID_SECOND_HALF_BOLD: 1,
|
||||
},
|
||||
|
||||
// sm.h
|
||||
|
||||
nextExpectedSMP: {
|
||||
OTRL_SMP_EXPECT1: 0,
|
||||
OTRL_SMP_EXPECT2: 1,
|
||||
OTRL_SMP_EXPECT3: 2,
|
||||
OTRL_SMP_EXPECT4: 3,
|
||||
OTRL_SMP_EXPECT5: 4,
|
||||
},
|
||||
|
||||
smProgState: {
|
||||
OTRL_SMP_PROG_OK: 0,
|
||||
OTRL_SMP_PROG_CHEATED: -2,
|
||||
OTRL_SMP_PROG_FAILED: -1,
|
||||
OTRL_SMP_PROG_SUCCEEDED: 1,
|
||||
},
|
||||
|
||||
// userstate.h
|
||||
|
||||
// Create a new OtrlUserState.
|
||||
otrl_userstate_create: libotr.declare(
|
||||
"otrl_userstate_create", abi, OtrlUserState
|
||||
),
|
||||
|
||||
// privkey.h
|
||||
|
||||
// Generate a private DSA key for a given account, storing it into a file on
|
||||
// disk, and loading it into the given OtrlUserState. Overwrite any
|
||||
// previously generated keys for that account in that OtrlUserState.
|
||||
otrl_privkey_generate: callWithFILEp.bind(null, "otrl_privkey_generate", "w+b", 1),
|
||||
otrl_privkey_generate_FILEp: libotr.declare(
|
||||
"otrl_privkey_generate_FILEp", abi, gcry_error_t,
|
||||
OtrlUserState, FILE.ptr, ctypes.char.ptr, ctypes.char.ptr
|
||||
),
|
||||
|
||||
// Begin a private key generation that will potentially take place in
|
||||
// a background thread. This routine must be called from the main
|
||||
// thread. It will set *newkeyp, which you can pass to
|
||||
// otrl_privkey_generate_calculate in a background thread. If it
|
||||
// returns gcry_error(GPG_ERR_EEXIST), then a privkey creation for
|
||||
// this accountname/protocol is already in progress, and *newkeyp will
|
||||
// be set to NULL.
|
||||
otrl_privkey_generate_start: libotr.declare(
|
||||
"otrl_privkey_generate_start", abi, gcry_error_t,
|
||||
OtrlUserState, ctypes.char.ptr, ctypes.char.ptr, ctypes.void_t.ptr.ptr
|
||||
),
|
||||
|
||||
// Do the private key generation calculation. You may call this from a
|
||||
// background thread. When it completes, call
|
||||
// otrl_privkey_generate_finish from the _main_ thread.
|
||||
otrl_privkey_generate_calculate: libotr.declare(
|
||||
"otrl_privkey_generate_calculate", abi, gcry_error_t,
|
||||
ctypes.void_t.ptr
|
||||
),
|
||||
|
||||
// Call this from the main thread only. It will write the newly created
|
||||
// private key into the given file and store it in the OtrlUserState.
|
||||
otrl_privkey_generate_finish: callWithFILEp.bind(null, "otrl_privkey_generate_finish", "w+b", 2),
|
||||
otrl_privkey_generate_finish_FILEp: libotr.declare(
|
||||
"otrl_privkey_generate_finish_FILEp", abi, gcry_error_t,
|
||||
OtrlUserState, ctypes.void_t.ptr, FILE.ptr
|
||||
),
|
||||
|
||||
// Call this from the main thread only, in the event that the background
|
||||
// thread generating the key is cancelled. The newkey is deallocated,
|
||||
// and must not be used further.
|
||||
otrl_privkey_generate_cancelled: libotr.declare(
|
||||
"otrl_privkey_generate_cancelled", abi, gcry_error_t,
|
||||
OtrlUserState, ctypes.void_t.ptr
|
||||
),
|
||||
|
||||
// Read a sets of private DSA keys from a file on disk into the given
|
||||
// OtrlUserState.
|
||||
otrl_privkey_read: callWithFILEp.bind(null, "otrl_privkey_read", "rb", 1),
|
||||
otrl_privkey_read_FILEp: libotr.declare(
|
||||
"otrl_privkey_read_FILEp", abi, gcry_error_t,
|
||||
OtrlUserState, FILE.ptr
|
||||
),
|
||||
|
||||
// Read the fingerprint store from a file on disk into the given
|
||||
// OtrlUserState.
|
||||
otrl_privkey_read_fingerprints: callWithFILEp.bind(null, "otrl_privkey_read_fingerprints", "rb", 1),
|
||||
otrl_privkey_read_fingerprints_FILEp: libotr.declare(
|
||||
"otrl_privkey_read_fingerprints_FILEp", abi, gcry_error_t,
|
||||
OtrlUserState, FILE.ptr, ctypes.void_t.ptr, ctypes.void_t.ptr
|
||||
),
|
||||
|
||||
// Write the fingerprint store from a given OtrlUserState to a file on disk.
|
||||
otrl_privkey_write_fingerprints: callWithFILEp.bind(null, "otrl_privkey_write_fingerprints", "wb", 1),
|
||||
otrl_privkey_write_fingerprints_FILEp: libotr.declare(
|
||||
"otrl_privkey_write_fingerprints_FILEp", abi, gcry_error_t,
|
||||
OtrlUserState, FILE.ptr
|
||||
),
|
||||
|
||||
// The length of a string representing a human-readable version of a
|
||||
// fingerprint (including the trailing NUL).
|
||||
OTRL_PRIVKEY_FPRINT_HUMAN_LEN,
|
||||
|
||||
// Human readable fingerprint type
|
||||
fingerprint_t,
|
||||
|
||||
// fingerprint value
|
||||
hash_t,
|
||||
|
||||
// Calculate a human-readable hash of our DSA public key. Return it in the
|
||||
// passed fingerprint buffer. Return NULL on error, or a pointer to the given
|
||||
// buffer on success.
|
||||
otrl_privkey_fingerprint: libotr.declare(
|
||||
"otrl_privkey_fingerprint", abi, ctypes.char.ptr,
|
||||
OtrlUserState, fingerprint_t, ctypes.char.ptr, ctypes.char.ptr
|
||||
),
|
||||
|
||||
// Convert a 20-byte hash value to a 45-byte human-readable value.
|
||||
otrl_privkey_hash_to_human: libotr.declare(
|
||||
"otrl_privkey_hash_to_human", abi, ctypes.void_t,
|
||||
fingerprint_t, hash_t
|
||||
),
|
||||
|
||||
// Calculate a raw hash of our DSA public key. Return it in the passed
|
||||
// fingerprint buffer. Return NULL on error, or a pointer to the given
|
||||
// buffer on success.
|
||||
otrl_privkey_fingerprint_raw: libotr.declare(
|
||||
"otrl_privkey_fingerprint_raw", abi, ctypes.unsigned_char.ptr,
|
||||
OtrlUserState, hash_t, ctypes.char.ptr, ctypes.char.ptr
|
||||
),
|
||||
|
||||
// uiOps callbacks
|
||||
policy_cb_t,
|
||||
create_privkey_cb_t,
|
||||
is_logged_in_cb_t,
|
||||
inject_message_cb_t,
|
||||
update_context_list_cb_t,
|
||||
new_fingerprint_cb_t,
|
||||
write_fingerprint_cb_t,
|
||||
gone_secure_cb_t,
|
||||
gone_insecure_cb_t,
|
||||
still_secure_cb_t,
|
||||
max_message_size_cb_t,
|
||||
account_name_cb_t,
|
||||
account_name_free_cb_t,
|
||||
received_symkey_cb_t,
|
||||
otr_error_message_cb_t,
|
||||
otr_error_message_free_cb_t,
|
||||
resent_msg_prefix_cb_t,
|
||||
resent_msg_prefix_free_cb_t,
|
||||
handle_smp_event_cb_t,
|
||||
handle_msg_event_cb_t,
|
||||
create_instag_cb_t,
|
||||
convert_msg_cb_t,
|
||||
convert_free_cb_t,
|
||||
timer_control_cb_t,
|
||||
|
||||
// message.h
|
||||
|
||||
OtrlMessageAppOps,
|
||||
|
||||
errorCode: {
|
||||
OTRL_ERRCODE_NONE: 0,
|
||||
OTRL_ERRCODE_ENCRYPTION_ERROR: 1,
|
||||
OTRL_ERRCODE_MSG_NOT_IN_PRIVATE: 2,
|
||||
OTRL_ERRCODE_MSG_UNREADABLE: 3,
|
||||
OTRL_ERRCODE_MSG_MALFORMED: 4,
|
||||
},
|
||||
|
||||
smpEvent: {
|
||||
OTRL_SMPEVENT_NONE: 0,
|
||||
OTRL_SMPEVENT_ERROR: 1,
|
||||
OTRL_SMPEVENT_ABORT: 2,
|
||||
OTRL_SMPEVENT_CHEATED: 3,
|
||||
OTRL_SMPEVENT_ASK_FOR_ANSWER: 4,
|
||||
OTRL_SMPEVENT_ASK_FOR_SECRET: 5,
|
||||
OTRL_SMPEVENT_IN_PROGRESS: 6,
|
||||
OTRL_SMPEVENT_SUCCESS: 7,
|
||||
OTRL_SMPEVENT_FAILURE: 8,
|
||||
},
|
||||
|
||||
messageEvent: {
|
||||
OTRL_MSGEVENT_NONE: 0,
|
||||
OTRL_MSGEVENT_ENCRYPTION_REQUIRED: 1,
|
||||
OTRL_MSGEVENT_ENCRYPTION_ERROR: 2,
|
||||
OTRL_MSGEVENT_CONNECTION_ENDED: 3,
|
||||
OTRL_MSGEVENT_SETUP_ERROR: 4,
|
||||
OTRL_MSGEVENT_MSG_REFLECTED: 5,
|
||||
OTRL_MSGEVENT_MSG_RESENT: 6,
|
||||
OTRL_MSGEVENT_RCVDMSG_NOT_IN_PRIVATE: 7,
|
||||
OTRL_MSGEVENT_RCVDMSG_UNREADABLE: 8,
|
||||
OTRL_MSGEVENT_RCVDMSG_MALFORMED: 9,
|
||||
OTRL_MSGEVENT_LOG_HEARTBEAT_RCVD: 10,
|
||||
OTRL_MSGEVENT_LOG_HEARTBEAT_SENT: 11,
|
||||
OTRL_MSGEVENT_RCVDMSG_GENERAL_ERR: 12,
|
||||
OTRL_MSGEVENT_RCVDMSG_UNENCRYPTED: 13,
|
||||
OTRL_MSGEVENT_RCVDMSG_UNRECOGNIZED: 14,
|
||||
OTRL_MSGEVENT_RCVDMSG_FOR_OTHER_INSTANCE: 15,
|
||||
},
|
||||
|
||||
convertType: {
|
||||
OTRL_CONVERT_SENDING: 0,
|
||||
OTRL_CONVERT_RECEIVING: 1,
|
||||
},
|
||||
|
||||
// Deallocate a message allocated by other otrl_message_* routines.
|
||||
otrl_message_free: libotr.declare(
|
||||
"otrl_message_free", abi, ctypes.void_t,
|
||||
ctypes.char.ptr
|
||||
),
|
||||
|
||||
// Handle a message about to be sent to the network.
|
||||
otrl_message_sending: libotr.declare(
|
||||
"otrl_message_sending", abi, gcry_error_t,
|
||||
OtrlUserState,
|
||||
OtrlMessageAppOps.ptr,
|
||||
ctypes.void_t.ptr,
|
||||
ctypes.char.ptr,
|
||||
ctypes.char.ptr,
|
||||
ctypes.char.ptr,
|
||||
otrl_instag_t,
|
||||
ctypes.char.ptr,
|
||||
OtrlTLV.ptr,
|
||||
ctypes.char.ptr.ptr,
|
||||
OtrlFragmentPolicy,
|
||||
ConnContext.ptr.ptr,
|
||||
ctypes.void_t.ptr,
|
||||
ctypes.void_t.ptr
|
||||
),
|
||||
|
||||
// Handle a message just received from the network.
|
||||
otrl_message_receiving: libotr.declare(
|
||||
"otrl_message_receiving", abi, ctypes.int,
|
||||
OtrlUserState,
|
||||
OtrlMessageAppOps.ptr,
|
||||
ctypes.void_t.ptr,
|
||||
ctypes.char.ptr,
|
||||
ctypes.char.ptr,
|
||||
ctypes.char.ptr,
|
||||
ctypes.char.ptr,
|
||||
ctypes.char.ptr.ptr,
|
||||
OtrlTLV.ptr.ptr,
|
||||
ConnContext.ptr.ptr,
|
||||
ctypes.void_t.ptr,
|
||||
ctypes.void_t.ptr
|
||||
),
|
||||
|
||||
// Put a connection into the PLAINTEXT state, first sending the
|
||||
// other side a notice that we're doing so if we're currently ENCRYPTED,
|
||||
// and we think he's logged in. Affects only the specified instance.
|
||||
otrl_message_disconnect: libotr.declare(
|
||||
"otrl_message_disconnect", abi, ctypes.void_t,
|
||||
OtrlUserState,
|
||||
OtrlMessageAppOps.ptr,
|
||||
ctypes.void_t.ptr,
|
||||
ctypes.char.ptr,
|
||||
ctypes.char.ptr,
|
||||
ctypes.char.ptr,
|
||||
otrl_instag_t
|
||||
),
|
||||
|
||||
// Call this function every so often, to clean up stale private state that
|
||||
// may otherwise stick around in memory.
|
||||
otrl_message_poll: libotr.declare(
|
||||
"otrl_message_poll", abi, ctypes.void_t,
|
||||
OtrlUserState,
|
||||
OtrlMessageAppOps.ptr,
|
||||
ctypes.void_t.ptr
|
||||
),
|
||||
|
||||
// Initiate the Socialist Millionaires' Protocol.
|
||||
otrl_message_initiate_smp: libotr.declare(
|
||||
"otrl_message_initiate_smp", abi, ctypes.void_t,
|
||||
OtrlUserState,
|
||||
OtrlMessageAppOps.ptr,
|
||||
ctypes.void_t.ptr,
|
||||
ConnContext.ptr,
|
||||
ctypes.char.ptr,
|
||||
ctypes.size_t
|
||||
),
|
||||
|
||||
// Initiate the Socialist Millionaires' Protocol and send a prompt
|
||||
// question to the buddy.
|
||||
otrl_message_initiate_smp_q: libotr.declare(
|
||||
"otrl_message_initiate_smp_q", abi, ctypes.void_t,
|
||||
OtrlUserState,
|
||||
OtrlMessageAppOps.ptr,
|
||||
ctypes.void_t.ptr,
|
||||
ConnContext.ptr,
|
||||
ctypes.char.ptr,
|
||||
ctypes.char.ptr,
|
||||
ctypes.size_t
|
||||
),
|
||||
|
||||
// Respond to a buddy initiating the Socialist Millionaires' Protocol.
|
||||
otrl_message_respond_smp: libotr.declare(
|
||||
"otrl_message_respond_smp", abi, ctypes.void_t,
|
||||
OtrlUserState,
|
||||
OtrlMessageAppOps.ptr,
|
||||
ctypes.void_t.ptr,
|
||||
ConnContext.ptr,
|
||||
ctypes.char.ptr,
|
||||
ctypes.size_t
|
||||
),
|
||||
|
||||
// Abort the SMP. Called when an unexpected SMP message breaks the
|
||||
// normal flow.
|
||||
otrl_message_abort_smp: libotr.declare(
|
||||
"otrl_message_abort_smp", abi, ctypes.void_t,
|
||||
OtrlUserState,
|
||||
OtrlMessageAppOps.ptr,
|
||||
ctypes.void_t.ptr,
|
||||
ConnContext.ptr
|
||||
),
|
||||
|
||||
// tlv.h
|
||||
|
||||
tlvs: {
|
||||
OTRL_TLV_PADDING: new ctypes.unsigned_short(0x0000),
|
||||
OTRL_TLV_DISCONNECTED: new ctypes.unsigned_short(0x0001),
|
||||
OTRL_TLV_SMP1: new ctypes.unsigned_short(0x0002),
|
||||
OTRL_TLV_SMP2: new ctypes.unsigned_short(0x0003),
|
||||
OTRL_TLV_SMP3: new ctypes.unsigned_short(0x0004),
|
||||
OTRL_TLV_SMP4: new ctypes.unsigned_short(0x0005),
|
||||
OTRL_TLV_SMP_ABORT: new ctypes.unsigned_short(0x0006),
|
||||
OTRL_TLV_SMP1Q: new ctypes.unsigned_short(0x0007),
|
||||
OTRL_TLV_SYMKEY: new ctypes.unsigned_short(0x0008),
|
||||
},
|
||||
|
||||
OtrlTLV,
|
||||
|
||||
// Return the first TLV with the given type in the chain, or NULL if one
|
||||
// isn't found.
|
||||
otrl_tlv_find: libotr.declare(
|
||||
"otrl_tlv_find", abi, OtrlTLV.ptr,
|
||||
OtrlTLV.ptr, ctypes.unsigned_short
|
||||
),
|
||||
|
||||
// Deallocate a chain of TLVs.
|
||||
otrl_tlv_free: libotr.declare(
|
||||
"otrl_tlv_free", abi, ctypes.void_t,
|
||||
OtrlTLV.ptr
|
||||
),
|
||||
|
||||
};
|
||||
|
||||
|
||||
// exports
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["OTRLib"];
|
|
@ -0,0 +1,711 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["OTRUI"];
|
||||
|
||||
const {Services} = ChromeUtils.import("resource:///modules/imServices.jsm");
|
||||
const {
|
||||
XPCOMUtils,
|
||||
l10nHelper,
|
||||
} = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
|
||||
|
||||
const {OTR} = ChromeUtils.import("resource:///modules/OTR.jsm");
|
||||
|
||||
const privDialog = "chrome://chat/content/otr-generate-key.xul";
|
||||
const authDialog = "chrome://chat/content/otr-auth.xul";
|
||||
const addFingerDialog = "chrome://chat/content/otr-add-fingerprint.xul";
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "_", () =>
|
||||
l10nHelper("chrome://chat/content/otrUI.properties")
|
||||
);
|
||||
|
||||
const authVerify = "otr-auth-unverified";
|
||||
|
||||
var authLabelMap = new Map([
|
||||
["otr:auth-error", _("auth.error")],
|
||||
["otr:auth-success", _("auth.success")],
|
||||
["otr:auth-successThem", _("auth.successThem")],
|
||||
["otr:auth-fail", _("auth.fail")],
|
||||
["otr:auth-waiting", _("auth.waiting")],
|
||||
]);
|
||||
|
||||
var authTitleMap = new Map([
|
||||
["otr:auth-error", "error"],
|
||||
["otr:auth-success", "success"],
|
||||
["otr:auth-successThem", "successThem"],
|
||||
["otr:auth-fail", "fail"],
|
||||
["otr:auth-waiting", "waiting"],
|
||||
]);
|
||||
|
||||
var trustMap = new Map([
|
||||
[OTR.trustState.TRUST_NOT_PRIVATE, {
|
||||
startLabel: _("start.label"),
|
||||
authLabel: _("auth.label"),
|
||||
disableStart: false,
|
||||
disableEnd: true,
|
||||
disableAuth: true,
|
||||
class: "not_private",
|
||||
}],
|
||||
[OTR.trustState.TRUST_UNVERIFIED, {
|
||||
startLabel: _("refresh.label"),
|
||||
authLabel: _("auth.label"),
|
||||
disableStart: false,
|
||||
disableEnd: false,
|
||||
disableAuth: false,
|
||||
class: "unverified",
|
||||
}],
|
||||
[OTR.trustState.TRUST_PRIVATE, {
|
||||
startLabel: _("refresh.label"),
|
||||
authLabel: _("reauth.label"),
|
||||
disableStart: false,
|
||||
disableEnd: false,
|
||||
disableAuth: false,
|
||||
class: "private",
|
||||
}],
|
||||
[OTR.trustState.TRUST_FINISHED, {
|
||||
startLabel: _("start.label"),
|
||||
authLabel: _("auth.label"),
|
||||
disableStart: false,
|
||||
disableEnd: false,
|
||||
disableAuth: true,
|
||||
class: "finished",
|
||||
}],
|
||||
]);
|
||||
|
||||
var windowRefs = new Map();
|
||||
|
||||
var OTRUI = {
|
||||
globalDoc: null,
|
||||
visibleConv: null,
|
||||
|
||||
debug: true,
|
||||
logMsg(msg) {
|
||||
if (!OTRUI.debug)
|
||||
return;
|
||||
Services.console.logStringMessage(msg);
|
||||
},
|
||||
|
||||
prefs: null,
|
||||
setPrefs() {
|
||||
let branch = "chat.otr.";
|
||||
let prefs = {
|
||||
requireEncryption: false,
|
||||
verifyNudge: true,
|
||||
};
|
||||
let defaults = Services.prefs.getDefaultBranch(branch);
|
||||
Object.keys(prefs).forEach(function(key) {
|
||||
defaults.setBoolPref(key, prefs[key]);
|
||||
});
|
||||
OTRUI.prefs = Services.prefs.getBranch(branch);
|
||||
},
|
||||
|
||||
addMenuObserver() {
|
||||
let iter = Services.ww.getWindowEnumerator();
|
||||
while (iter.hasMoreElements())
|
||||
OTRUI.addMenus(iter.getNext());
|
||||
Services.obs.addObserver(OTRUI, "domwindowopened");
|
||||
},
|
||||
|
||||
removeMenuObserver() {
|
||||
let iter = Services.ww.getWindowEnumerator();
|
||||
while (iter.hasMoreElements())
|
||||
OTRUI.removeMenus(iter.getNext());
|
||||
Services.obs.removeObserver(OTRUI, "domwindowopened");
|
||||
},
|
||||
|
||||
addMenus(win) {
|
||||
let doc = win.document;
|
||||
// Account for unready windows
|
||||
if (doc.readyState !== "complete") {
|
||||
let listen = function() {
|
||||
win.removeEventListener("load", listen);
|
||||
OTRUI.addMenus(win);
|
||||
};
|
||||
win.addEventListener("load", listen);
|
||||
}
|
||||
},
|
||||
|
||||
removeMenus(win) {
|
||||
let doc = win.document;
|
||||
OTRUI.removeBuddyContextMenu(doc);
|
||||
},
|
||||
|
||||
addBuddyContextMenu(buddyContextMenu, doc) {
|
||||
if (!buddyContextMenu || !OTR.libLoaded) {
|
||||
return; // Not the buddy list context menu
|
||||
}
|
||||
OTRUI.removeBuddyContextMenu(doc);
|
||||
|
||||
let sep = doc.createElement("menuseparator");
|
||||
sep.setAttribute("id", "otrsep");
|
||||
let menuitem = doc.createElement("menuitem");
|
||||
menuitem.setAttribute("label", _("buddycontextmenu.label"));
|
||||
menuitem.setAttribute("id", "otrcont");
|
||||
menuitem.addEventListener("command", () => {
|
||||
let target = buddyContextMenu.triggerNode;
|
||||
if (target.localName == "richlistitem") {
|
||||
let contact = target.contact;
|
||||
let args = OTRUI.contactWrapper(contact);
|
||||
args.wrappedJSObject = args;
|
||||
let features = "chrome,modal,centerscreen,resizable=no,minimizable=no";
|
||||
Services.ww.openWindow(null, addFingerDialog, "", features, args);
|
||||
}
|
||||
});
|
||||
|
||||
buddyContextMenu.addEventListener("popupshowing", (e) => {
|
||||
let target = e.target.triggerNode;
|
||||
if (target.localName == "richlistitem") {
|
||||
menuitem.hidden = false;
|
||||
sep.hidden = false;
|
||||
} else { /* probably imconv */
|
||||
menuitem.hidden = true;
|
||||
sep.hidden = true;
|
||||
}
|
||||
});
|
||||
|
||||
buddyContextMenu.appendChild(sep);
|
||||
buddyContextMenu.appendChild(menuitem);
|
||||
},
|
||||
|
||||
removeBuddyContextMenu(doc) {
|
||||
let s = doc.getElementById("otrsep");
|
||||
if (s) {
|
||||
s.remove();
|
||||
}
|
||||
let p = doc.getElementById("otrcont");
|
||||
if (p) {
|
||||
p.remove();
|
||||
}
|
||||
},
|
||||
|
||||
init() {
|
||||
// console.log("====> OTRUI init\n");
|
||||
OTRUI.setPrefs();
|
||||
OTR.init({
|
||||
requireEncryption: OTRUI.prefs.getBoolPref("requireEncryption"),
|
||||
verifyNudge: OTRUI.prefs.getBoolPref("verifyNudge"),
|
||||
});
|
||||
if (!OTR.libLoaded) {
|
||||
return;
|
||||
}
|
||||
OTR.addObserver(OTRUI);
|
||||
OTR.loadFiles().then(function() {
|
||||
Services.obs.addObserver(OTR, "new-ui-conversation");
|
||||
// Disabled until #76 is resolved.
|
||||
// Services.obs.addObserver(OTRUI, "contact-added", false);
|
||||
Services.obs.addObserver(OTRUI, "account-added");
|
||||
// Services.obs.addObserver(OTRUI, "contact-signed-off", false);
|
||||
Services.obs.addObserver(OTRUI, "conversation-loaded");
|
||||
Services.obs.addObserver(OTRUI, "conversation-closed");
|
||||
Services.obs.addObserver(OTRUI, "prpl-quit");
|
||||
|
||||
OTRUI.prefs.addObserver("", OTRUI);
|
||||
let conversations = Services.conversations.getConversations();
|
||||
while (conversations.hasMoreElements()) {
|
||||
let aConv = conversations.getNext();
|
||||
OTRUI.initConv(aConv);
|
||||
}
|
||||
OTRUI.addMenuObserver();
|
||||
}).catch(function(err) {
|
||||
// console.log("===> " + err + "\n");
|
||||
throw err;
|
||||
});
|
||||
},
|
||||
|
||||
disconnect(aConv) {
|
||||
if (aConv)
|
||||
return OTR.disconnect(aConv, true);
|
||||
let allGood = true;
|
||||
let conversations = Services.conversations.getConversations();
|
||||
while (conversations.hasMoreElements()) {
|
||||
let conv = conversations.getNext();
|
||||
if (conv.isChat)
|
||||
continue;
|
||||
if (!OTR.disconnect(conv, true)) {
|
||||
allGood = false;
|
||||
}
|
||||
}
|
||||
return allGood;
|
||||
},
|
||||
|
||||
changePref(aMsg) {
|
||||
switch (aMsg) {
|
||||
case "requireEncryption":
|
||||
OTR.setPolicy(OTRUI.prefs.getBoolPref("requireEncryption"));
|
||||
break;
|
||||
case "verifyNudge":
|
||||
OTR.verifyNudge = OTRUI.prefs.getBoolPref("verifyNudge");
|
||||
break;
|
||||
default:
|
||||
OTRUI.logMsg(aMsg);
|
||||
}
|
||||
},
|
||||
|
||||
openAuth(window, name, mode, uiConv, contactInfo) {
|
||||
let otrAuth = this.globalDoc.querySelector(".otr-auth");
|
||||
otrAuth.disabled = true;
|
||||
let win = window.openDialog(
|
||||
authDialog,
|
||||
"auth=" + name,
|
||||
"centerscreen,resizable=no,minimizable=no",
|
||||
mode,
|
||||
uiConv,
|
||||
contactInfo
|
||||
);
|
||||
windowRefs.set(name, win);
|
||||
window.addEventListener("beforeunload", function() {
|
||||
otrAuth.disabled = false;
|
||||
windowRefs.delete(name);
|
||||
});
|
||||
},
|
||||
|
||||
closeAuth(context) {
|
||||
let win = windowRefs.get(context.username);
|
||||
if (win)
|
||||
win.close();
|
||||
},
|
||||
|
||||
noOtrPossible(otrContainer, context) {
|
||||
otrContainer.hidden = true;
|
||||
|
||||
if (context) {
|
||||
OTRUI.hideUserNotifications(context);
|
||||
} else {
|
||||
OTRUI.hideAllNotifications();
|
||||
}
|
||||
},
|
||||
|
||||
sendSystemAlert(uiConv, conv, bundleId) {
|
||||
uiConv.systemMessage(_(bundleId, conv.normalizedName));
|
||||
},
|
||||
|
||||
setNotificationBox(notificationbox) {
|
||||
this.globalBox = notificationbox;
|
||||
},
|
||||
|
||||
/*
|
||||
* possible states:
|
||||
* tab isn't a 1:1, isChat == true
|
||||
* then OTR isn't possible, hide the button
|
||||
* tab is a 1:1, isChat == false
|
||||
* no conversation active, uiConv cannot be found
|
||||
* then OTR isn't possible YET, hide the button
|
||||
* conversation active, uiConv found
|
||||
* disconnected?
|
||||
* could the other side come back? should we keep the button?
|
||||
* set the state based on the OTR library state
|
||||
*/
|
||||
|
||||
addButton(aObject) {
|
||||
this.globalDoc = aObject.ownerDocument;
|
||||
let _conv = aObject._conv;
|
||||
OTRUI.setMsgState(_conv, null, this.globalDoc, true);
|
||||
},
|
||||
|
||||
hideOTRButton() {
|
||||
if (!OTR.libLoaded)
|
||||
return;
|
||||
if (!this.globalDoc)
|
||||
return;
|
||||
OTRUI.visibleConv = null;
|
||||
let otrContainer = this.globalDoc.querySelector(".otr-container");
|
||||
OTRUI.noOtrPossible(otrContainer);
|
||||
},
|
||||
|
||||
updateOTRButton(_conv) {
|
||||
if (!OTR.libLoaded)
|
||||
return;
|
||||
if (!this.globalDoc)
|
||||
return;
|
||||
OTRUI.visibleConv = _conv;
|
||||
let convBinding =
|
||||
this.globalDoc.getElementById("conversationsDeck").selectedPanel;
|
||||
if (convBinding && convBinding._conv && convBinding._conv.target) {
|
||||
OTRUI.setMsgState(_conv, null, this.globalDoc, false);
|
||||
} else {
|
||||
this.hideOTRButton();
|
||||
}
|
||||
},
|
||||
|
||||
// set msg state on toolbar button
|
||||
setMsgState(_conv, context, doc, addSystemMessage) {
|
||||
if (!this.visibleConv) {
|
||||
return;
|
||||
}
|
||||
if (_conv != null && !(_conv === this.visibleConv)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let otrContainer = doc.querySelector(".otr-container");
|
||||
let otrButton = doc.querySelector(".otr-button");
|
||||
if (_conv != null && _conv.isChat) {
|
||||
OTRUI.noOtrPossible(otrContainer, context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context && _conv != null) {
|
||||
context = OTR.getContext(_conv);
|
||||
if (!context) {
|
||||
OTRUI.noOtrPossible(otrContainer, null);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let uiConv = OTR.getUIConvFromContext(context);
|
||||
if (uiConv != null && !(uiConv === this.visibleConv)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (uiConv.isChat) {
|
||||
OTRUI.noOtrPossible(otrContainer, context);
|
||||
return;
|
||||
}
|
||||
if (addSystemMessage) {
|
||||
let trust = OTRUI.getTrustSettings(context);
|
||||
uiConv.systemMessage(_("state." + trust.class, context.username));
|
||||
}
|
||||
} catch (e) {
|
||||
OTRUI.noOtrPossible(otrContainer, context);
|
||||
return;
|
||||
}
|
||||
|
||||
otrContainer.hidden = false;
|
||||
let otrStart = doc.querySelector(".otr-start");
|
||||
let otrEnd = doc.querySelector(".otr-end");
|
||||
let otrAuth = doc.querySelector(".otr-auth");
|
||||
let trust = OTRUI.getTrustSettings(context);
|
||||
otrButton.setAttribute("tooltiptext", _("state." + trust.class, context.username));
|
||||
otrButton.setAttribute("label", _("state." + trust.class + ".label"));
|
||||
otrButton.className = "otr-button otr-" + trust.class;
|
||||
otrStart.setAttribute("label", trust.startLabel);
|
||||
otrStart.setAttribute("disabled", trust.disableStart);
|
||||
otrEnd.setAttribute("disabled", trust.disableEnd);
|
||||
otrAuth.setAttribute("label", trust.authLabel);
|
||||
otrAuth.setAttribute("disabled", trust.disableAuth);
|
||||
OTRUI.hideAllNotifications();
|
||||
OTRUI.showUserNotifications(context);
|
||||
},
|
||||
|
||||
alertTrust(context) {
|
||||
let uiConv = OTR.getUIConvFromContext(context);
|
||||
let trust = OTRUI.getTrustSettings(context);
|
||||
uiConv.systemMessage(_("afterauth." + trust.class, context.username));
|
||||
},
|
||||
|
||||
getTrustSettings(context) {
|
||||
let result = trustMap.get(OTR.trust(context));
|
||||
return result;
|
||||
},
|
||||
|
||||
askAuth(aObject) {
|
||||
let uiConv = OTR.getUIConvFromContext(aObject.context);
|
||||
if (!uiConv) return;
|
||||
|
||||
let window = this.globalDoc.defaultView;
|
||||
let name = uiConv.target.normalizedName;
|
||||
OTRUI.openAuth(window, name, "ask", uiConv, aObject);
|
||||
},
|
||||
|
||||
closeUnverified(context) {
|
||||
let uiConv = OTR.getUIConvFromContext(context);
|
||||
if (!uiConv) return;
|
||||
|
||||
let notification = this.globalBox.getNotificationWithValue(authVerify);
|
||||
if (notification)
|
||||
notification.close();
|
||||
},
|
||||
|
||||
hideUserNotifications(context) {
|
||||
let notifications = this.globalBox.allNotifications;
|
||||
for (let i = notifications.length - 1; i >= 0; i--) {
|
||||
if (context.username == notifications[i].getAttribute("user")) {
|
||||
notifications[i].setAttribute("hidden", "true");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
hideAllNotifications() {
|
||||
let notifications = this.globalBox.allNotifications;
|
||||
for (let i = notifications.length - 1; i >= 0; i--) {
|
||||
notifications[i].setAttribute("hidden", "true");
|
||||
}
|
||||
},
|
||||
|
||||
showUserNotifications(context) {
|
||||
let notifications = this.globalBox.allNotifications;
|
||||
for (let i = notifications.length - 1; i >= 0; i--) {
|
||||
if (context.username == notifications[i].getAttribute("user"))
|
||||
notifications[i].removeAttribute("hidden");
|
||||
}
|
||||
},
|
||||
|
||||
notifyUnverified(context, seen) {
|
||||
let uiConv = OTR.getUIConvFromContext(context);
|
||||
if (!uiConv) return;
|
||||
|
||||
if (this.globalBox.getNotificationWithValue(authVerify))
|
||||
return;
|
||||
|
||||
let window = this.globalDoc.defaultView;
|
||||
|
||||
let msg = _("finger." + seen, context.username);
|
||||
let buttons = [{
|
||||
label: _("finger.verify"),
|
||||
accessKey: _("verify.accessKey"),
|
||||
callback() {
|
||||
let name = uiConv.target.normalizedName;
|
||||
OTRUI.openAuth(window, name, "start", uiConv);
|
||||
// prevent closing of notification bar when the button is hit
|
||||
return true;
|
||||
},
|
||||
}];
|
||||
|
||||
let priority = this.globalBox.PRIORITY_WARNING_MEDIUM;
|
||||
this.globalBox.appendNotification(msg, authVerify, null, priority, buttons, null);
|
||||
|
||||
this.updateNotificationUI(context, "verify", authVerify);
|
||||
},
|
||||
|
||||
updateNotificationUI(context, type, value) {
|
||||
let notification = this.globalBox.getNotificationWithValue(value);
|
||||
notification.setAttribute("user", context.username);
|
||||
notification.setAttribute("orient", "vertical");
|
||||
notification.messageDetails.setAttribute("orient", "vertical");
|
||||
notification.messageDetails.removeAttribute("oncommand");
|
||||
notification.messageDetails.removeAttribute("align");
|
||||
|
||||
let title = this.globalDoc.createElement("title");
|
||||
title.setAttribute("flex", "1");
|
||||
title.setAttribute("crop", "end");
|
||||
title.textContent = _(type + ".title");
|
||||
|
||||
let close = notification.querySelector("toolbarbutton");
|
||||
close.setAttribute("oncommand", "this.parentNode.parentNode.dismiss();");
|
||||
|
||||
let top = this.globalDoc.createElement("hbox");
|
||||
top.setAttribute("flex", "1");
|
||||
top.setAttribute("align", "center");
|
||||
top.classList.add("otr-notification-header");
|
||||
top.appendChild(notification.messageImage);
|
||||
top.appendChild(title);
|
||||
top.appendChild(close);
|
||||
notification.insertBefore(top, notification.messageDetails);
|
||||
|
||||
let bottom = this.globalDoc.createElement("hbox");
|
||||
bottom.setAttribute("flex", "1");
|
||||
bottom.setAttribute("oncommand", "this.parentNode._doButtonCommand(event);");
|
||||
bottom.classList.add("otr-notification-footer");
|
||||
|
||||
notification.querySelectorAll("button").forEach((e) => {
|
||||
bottom.appendChild(e);
|
||||
});
|
||||
|
||||
notification.appendChild(bottom);
|
||||
},
|
||||
|
||||
closeVerification(context) {
|
||||
let uiConv = OTR.getUIConvFromContext(context);
|
||||
if (!uiConv) return;
|
||||
|
||||
authLabelMap.forEach(function(_, key) {
|
||||
let prevNotification = OTRUI.globalBox.getNotificationWithValue(key);
|
||||
if (prevNotification)
|
||||
prevNotification.close();
|
||||
});
|
||||
},
|
||||
|
||||
notifyVerification(context, key, cancelable) {
|
||||
let uiConv = OTR.getUIConvFromContext(context);
|
||||
if (!uiConv) return;
|
||||
|
||||
// TODO: maybe update the .label property on the notification instead
|
||||
// of closing it ... although, buttons need to be updated too.
|
||||
OTRUI.closeVerification(context);
|
||||
|
||||
let msg = authLabelMap.get(key);
|
||||
let type = authTitleMap.get(key);
|
||||
let buttons = [];
|
||||
if (cancelable) {
|
||||
buttons = [{
|
||||
label: _("auth.cancel"),
|
||||
accessKey: _("auth.cancelAccessKey"),
|
||||
callback() {
|
||||
let context = OTR.getContext(uiConv.target);
|
||||
OTR.abortSMP(context);
|
||||
},
|
||||
}];
|
||||
}
|
||||
|
||||
// higher priority to overlay the current notifyUnverified
|
||||
let priority = this.globalBox.PRIORITY_WARNING_HIGH;
|
||||
OTRUI.closeUnverified(context);
|
||||
this.globalBox.appendNotification(msg, key, null, priority, buttons, null);
|
||||
|
||||
this.updateNotificationUI(context, type, key);
|
||||
},
|
||||
|
||||
updateAuth(aObj) {
|
||||
// let uiConv = OTR.getUIConvFromContext(aObj.context);
|
||||
if (!aObj.progress) {
|
||||
OTRUI.closeAuth(aObj.context);
|
||||
OTRUI.notifyVerification(aObj.context, "otr:auth-error", false);
|
||||
} else if (aObj.progress === 100) {
|
||||
let key;
|
||||
if (aObj.success) {
|
||||
if (aObj.context.trust) {
|
||||
key = "otr:auth-success";
|
||||
OTR.notifyTrust(aObj.context);
|
||||
} else {
|
||||
key = "otr:auth-successThem";
|
||||
}
|
||||
} else {
|
||||
key = "otr:auth-fail";
|
||||
if (!aObj.context.trust)
|
||||
OTR.notifyTrust(aObj.context);
|
||||
}
|
||||
OTRUI.notifyVerification(aObj.context, key, false);
|
||||
} else {
|
||||
// TODO: show the aObj.progress to the user with a
|
||||
// <progressmeter mode="determined" value="10" />
|
||||
OTRUI.notifyVerification(aObj.context, "otr:auth-waiting", true);
|
||||
}
|
||||
},
|
||||
|
||||
generate(args) {
|
||||
let features = "chrome,modal,centerscreen,resizable=no,minimizable=no";
|
||||
args.wrappedJSObject = args;
|
||||
Services.ww.openWindow(null, privDialog, "", features, args);
|
||||
},
|
||||
|
||||
onAccountCreated(acc) {
|
||||
let account = acc.normalizedName;
|
||||
let protocol = acc.protocol.normalizedName;
|
||||
Promise.resolve();
|
||||
if (OTR.privateKeyFingerprint(account, protocol) === null)
|
||||
OTR.generatePrivateKey(account, protocol);
|
||||
},
|
||||
|
||||
contactWrapper(contact) {
|
||||
let wrapper = {
|
||||
account: contact.preferredBuddy.preferredAccountBuddy.account.normalizedName,
|
||||
protocol: contact.preferredBuddy.protocol.normalizedName,
|
||||
screenname: contact.preferredBuddy.preferredAccountBuddy.userName,
|
||||
};
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
onContactAdded(contact) {
|
||||
let args = OTRUI.contactWrapper(contact);
|
||||
if (OTR.getFingerprintsForRecipient(args.account, args.protocol, args.screenname).length > 0)
|
||||
return;
|
||||
args.wrappedJSObject = args;
|
||||
let features = "chrome,modal,centerscreen,resizable=no,minimizable=no";
|
||||
Services.ww.openWindow(null, addFingerDialog, "", features, args);
|
||||
},
|
||||
|
||||
observe(aObject, aTopic, aMsg) {
|
||||
let doc;
|
||||
// console.log("====> observing topic: " + aTopic + " with msg: " + aMsg);
|
||||
// console.log(aObject);
|
||||
|
||||
switch (aTopic) {
|
||||
case "nsPref:changed":
|
||||
OTRUI.changePref(aMsg);
|
||||
break;
|
||||
case "conversation-loaded":
|
||||
doc = aObject.ownerDocument;
|
||||
let windowtype = doc.documentElement.getAttribute("windowtype");
|
||||
if (windowtype !== "mail:3pane") {
|
||||
return;
|
||||
}
|
||||
OTRUI.addButton(aObject);
|
||||
break;
|
||||
case "conversation-closed":
|
||||
if (aObject.isChat)
|
||||
return;
|
||||
this.globalBox.removeAllNotifications();
|
||||
OTRUI.closeAuth(OTR.getContext(aObject));
|
||||
OTRUI.disconnect(aObject);
|
||||
break;
|
||||
// case "contact-signed-off":
|
||||
// break;
|
||||
case "prpl-quit":
|
||||
OTRUI.disconnect(null);
|
||||
break;
|
||||
case "domwindowopened":
|
||||
OTRUI.addMenus(aObject);
|
||||
break;
|
||||
case "otr:generate":
|
||||
OTRUI.generate(aObject);
|
||||
break;
|
||||
case "otr:disconnected":
|
||||
case "otr:msg-state":
|
||||
if (aTopic === "otr:disconnected" ||
|
||||
OTR.trust(aObject) !== OTR.trustState.TRUST_UNVERIFIED) {
|
||||
OTRUI.closeAuth(aObject);
|
||||
OTRUI.closeUnverified(aObject);
|
||||
OTRUI.closeVerification(aObject);
|
||||
}
|
||||
OTRUI.setMsgState(null, aObject, this.globalDoc, false);
|
||||
break;
|
||||
case "otr:unverified":
|
||||
OTRUI.notifyUnverified(aObject, aMsg);
|
||||
break;
|
||||
case "otr:trust-state":
|
||||
OTRUI.alertTrust(aObject);
|
||||
break;
|
||||
case "otr:log":
|
||||
OTRUI.logMsg("otr: " + aObject);
|
||||
break;
|
||||
case "account-added":
|
||||
OTRUI.onAccountCreated(aObject);
|
||||
break;
|
||||
case "contact-added":
|
||||
OTRUI.onContactAdded(aObject);
|
||||
break;
|
||||
case "otr:auth-ask":
|
||||
OTRUI.askAuth(aObject);
|
||||
break;
|
||||
case "otr:auth-update":
|
||||
OTRUI.updateAuth(aObject);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
initConv(binding) {
|
||||
OTR.addConversation(binding._conv);
|
||||
OTRUI.addButton(binding);
|
||||
},
|
||||
|
||||
resetConv(binding) {
|
||||
OTR.removeConversation(binding._conv);
|
||||
let otrButton = this.globalDoc.querySelector(".otr-button");
|
||||
if (!otrButton)
|
||||
return;
|
||||
otrButton.remove();
|
||||
},
|
||||
|
||||
destroy() {
|
||||
if (!OTR.libLoaded)
|
||||
return;
|
||||
OTRUI.disconnect(null);
|
||||
Services.obs.removeObserver(OTR, "new-ui-conversation");
|
||||
// Services.obs.removeObserver(OTRUI, "contact-added");
|
||||
// Services.obs.removeObserver(OTRUI, "contact-signed-off");
|
||||
Services.obs.removeObserver(OTRUI, "account-added");
|
||||
Services.obs.removeObserver(OTRUI, "conversation-loaded");
|
||||
Services.obs.removeObserver(OTRUI, "conversation-closed");
|
||||
Services.obs.removeObserver(OTRUI, "prpl-quit");
|
||||
|
||||
let conversations = Services.conversations.getConversations();
|
||||
while (conversations.hasMoreElements()) {
|
||||
OTRUI.resetConv(conversations.getNext());
|
||||
}
|
||||
OTRUI.prefs.removeObserver("", OTRUI);
|
||||
OTR.removeObserver(OTRUI);
|
||||
OTR.close();
|
||||
OTRUI.removeMenuObserver();
|
||||
},
|
||||
|
||||
};
|
|
@ -8,6 +8,7 @@ XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
|
|||
EXTRA_JS_MODULES += [
|
||||
'ArrayBufferUtils.jsm',
|
||||
'BigInteger.jsm',
|
||||
'CLib.jsm',
|
||||
'DNS.jsm',
|
||||
'hiddenWindow.jsm',
|
||||
'imContentSink.jsm',
|
||||
|
@ -19,6 +20,10 @@ EXTRA_JS_MODULES += [
|
|||
'imXPCOMUtils.jsm',
|
||||
'jsProtoHelper.jsm',
|
||||
'NormalizedMap.jsm',
|
||||
'OTR.jsm',
|
||||
'OTRHelpers.jsm',
|
||||
'OTRLib.jsm',
|
||||
'OTRUI.jsm',
|
||||
'socket.jsm',
|
||||
'ToLocaleFormat.jsm',
|
||||
]
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="context-fill" d="M12,7 L13,7 C13.5522847,7 14,7.44771525 14,8 L14,14 C14,14.5522847 13.5522847,15 13,15 L3,15 C2.44771525,15 2,14.5522847 2,14 L2,8 C2,7.44771525 2.44771525,7 3,7 L4,7 L4,5.00032973 C4,2.79202307 5.79321704,1 8,1 C10.2075938,1 12,2.79481161 12,5.00032973 L12,7 Z M10,7 L10,5.00032973 C10,3.89878113 9.10242341,3 8,3 C6.89748845,3 6,3.89689088 6,5.00032973 L6,7 L10,7 Z"/>
|
||||
<path style="fill:#00b22c;fill-opacity:1;stroke-width:0.99999988" d="M 9.0226853,15.999039 A 0.8763328,0.8763328 0 0 1 8.403118,15.742273 L 5.7741199,13.113275 A 0.8763328,0.8763328 0 0 1 7.0132545,11.87414 l 1.8902496,1.89025 5.5349179,-7.9071509 a 0.8763328,0.8763328 0 0 1 1.436309,1.0042774 L 9.7404016,15.624844 a 0.8763328,0.8763328 0 0 1 -0.6414753,0.374195 0.75627521,0.75627521 0 0 1 -0.076241,0 z"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 1.1 KiB |
|
@ -0,0 +1,7 @@
|
|||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="context-fill" d="M12,7 L13,7 C13.5522847,7 14,7.44771525 14,8 L14,14 C14,14.5522847 13.5522847,15 13,15 L3,15 C2.44771525,15 2,14.5522847 2,14 L2,8 C2,7.44771525 2.44771525,7 3,7 L4,7 L4,5.00032973 C4,2.79202307 5.79321704,1 8,1 C10.2075938,1 12,2.79481161 12,5.00032973 L12,7 Z M10,7 L10,5.00032973 C10,3.89878113 9.10242341,3 8,3 C6.89748845,3 6,3.89689088 6,5.00032973 L6,7 L10,7 Z"/>
|
||||
<path d="M 11.936582,10.734873 15.767811,6.9036441 A 0.8280753,0.8280753 0 0 0 14.59636,5.7332977 L 10.765132,9.5634223 6.9339034,5.7332977 A 0.8280753,0.8280753 0 1 0 5.763557,6.9036441 L 9.5936809,10.734873 5.763557,14.566099 a 0.8280753,0.8280753 0 1 0 1.1703464,1.170347 l 3.8312286,-3.830123 3.831228,3.831227 a 0.8280753,0.8280753 0 0 0 1.170347,-1.171451 z" style="fill:#ff9400;fill-opacity:1;stroke-width:1"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 1.1 KiB |
|
@ -20,9 +20,12 @@ chat.jar:
|
|||
skin/classic/chat/browserRequest.css
|
||||
skin/classic/chat/imtooltip.css
|
||||
skin/classic/chat/status.css
|
||||
skin/classic/chat/otr.css
|
||||
skin/classic/chat/prpl-generic/icon32.png (icons/prpl-generic-32.png)
|
||||
skin/classic/chat/prpl-generic/icon48.png (icons/prpl-generic-48.png)
|
||||
skin/classic/chat/prpl-generic/icon.png (icons/prpl-generic.png)
|
||||
skin/classic/chat/prpl-unknown/icon32.png (icons/prpl-unknown-32.png)
|
||||
skin/classic/chat/prpl-unknown/icon48.png (icons/prpl-unknown-48.png)
|
||||
skin/classic/chat/prpl-unknown/icon.png (icons/prpl-unknown.png)
|
||||
skin/classic/chat/otr-connection-encrypted.svg (icons/otr-connection-encrypted.svg)
|
||||
skin/classic/chat/otr-connection-finished.svg (icons/otr-connection-finished.svg)
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
.otr-container {
|
||||
border-top: 1px solid var(--splitter-color);
|
||||
min-height: 32px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.otr-label {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.otr-not_private > image {
|
||||
list-style-image: url("chrome://messenger/skin/icons/connection-insecure.svg");
|
||||
}
|
||||
|
||||
.otr-unverified > image {
|
||||
list-style-image: url("chrome://messenger/skin/icons/connection-mixed.svg");
|
||||
}
|
||||
|
||||
.otr-finished > image {
|
||||
list-style-image: url("chrome://chat/skin/otr-connection-finished.svg");
|
||||
}
|
||||
|
||||
.otr-private > image {
|
||||
list-style-image: url("chrome://chat/skin/otr-connection-encrypted.svg");
|
||||
}
|
||||
|
||||
toolbarbutton.otr-button {
|
||||
-moz-appearance: button !important;
|
||||
padding: 1px !important;
|
||||
}
|
||||
|
||||
.otr-button > image {
|
||||
margin-inline-end: 3px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.otr-button .toolbarbutton-menu-dropmarker {
|
||||
-moz-appearance: none !important;
|
||||
list-style-image: none;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-inline-start: 3px;
|
||||
width: 9px;
|
||||
}
|
||||
|
||||
.otr-button .toolbarbutton-menu-dropmarker > .dropmarker-icon {
|
||||
width: 17px;
|
||||
height: 7px;
|
||||
background-image: url("chrome://messenger/skin/icons/toolbarbutton-arrow.svg");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 9px 7px;
|
||||
}
|
||||
|
||||
/* otr botificationbox tweaks */
|
||||
|
||||
#otr-notification-box notification[type="warning"] {
|
||||
background: #FFF2BE !important;
|
||||
}
|
||||
|
||||
#otr-notification-box notification {
|
||||
padding-inline-start: 6px !important;
|
||||
padding: 6px !important;
|
||||
border-top: 1px solid var(--splitter-color) !important;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
#otr-notification-box .messageImage {
|
||||
margin-inline-end: 6px !important;
|
||||
color: #9E650C;
|
||||
}
|
||||
|
||||
#otr-notification-box .messageText {
|
||||
margin-inline-start: 6px !important;
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
|
||||
.otr-notification-header {
|
||||
display: inherit;
|
||||
padding: 3px 6px;
|
||||
}
|
||||
|
||||
.otr-notification-header title {
|
||||
font-weight: bold;
|
||||
color: #9E650C;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.otr-notification-header .messageCloseButton {
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.otr-notification-header .messageCloseButton > .toolbarbutton-icon {
|
||||
margin-inline-end: 0px !important;
|
||||
}
|
||||
|
||||
.otr-notification-footer {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
/* waiting */
|
||||
#otr-notification-box notification[type="warning"][value="otr:auth-waiting"] >
|
||||
hbox > .messageImage {
|
||||
list-style-image: url("chrome://global/skin/icons/help.svg") !important;
|
||||
}
|
||||
|
||||
/* fail */
|
||||
#otr-notification-box notification[type="warning"][value="otr:auth-fail"] {
|
||||
background: #ffc9d5 !important;
|
||||
}
|
||||
|
||||
#otr-notification-box notification[type="warning"][value="otr:auth-fail"] >
|
||||
hbox > .messageImage {
|
||||
list-style-image: url("chrome://global/skin/icons/error.svg") !important;
|
||||
color: #c93434 !important;
|
||||
}
|
||||
|
||||
#otr-notification-box notification[type="warning"][value="otr:auth-fail"]
|
||||
.otr-notification-header title {
|
||||
color: #c93434 !important;
|
||||
}
|
||||
|
||||
/* success */
|
||||
#otr-notification-box notification[type="warning"][value="otr:auth-success"] {
|
||||
background: #D3F4AF !important;
|
||||
}
|
||||
|
||||
#otr-notification-box notification[type="warning"][value="otr:auth-success"] >
|
||||
hbox > .messageImage {
|
||||
list-style-image: url("chrome://global/skin/icons/check.svg") !important;
|
||||
color: #407501 !important;
|
||||
}
|
||||
|
||||
#otr-notification-box notification[type="warning"][value="otr:auth-success"]
|
||||
.otr-notification-header title {
|
||||
color: #407501 !important;
|
||||
}
|
|
@ -719,6 +719,8 @@ pref("mail.chat.play_sound", true);
|
|||
pref("mail.chat.play_sound.type", 0);
|
||||
// if sound is user specified, this needs to be a file url
|
||||
pref("mail.chat.play_sound.url", "");
|
||||
// Enable/Disable support for OTR chat encryption.
|
||||
pref("chat.otr.enable", false);
|
||||
|
||||
// BigFiles
|
||||
pref("mail.cloud_files.enabled", true);
|
||||
|
|
|
@ -4,7 +4,21 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
/* global MozXULElement */
|
||||
var {Services} = ChromeUtils.import("resource:///modules/imServices.jsm");
|
||||
var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
ChromeUtils.defineModuleGetter(this, "OTR", "resource:///modules/OTR.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "OTRUI", "resource:///modules/OTRUI.jsm");
|
||||
|
||||
/* globals MozElements MozXULElement */
|
||||
|
||||
const gNotification = {};
|
||||
XPCOMUtils.defineLazyGetter(gNotification, "notificationbox", () => {
|
||||
return new MozElements.NotificationBox(element => {
|
||||
element.setAttribute("flex", "1");
|
||||
document.getElementById("otr-notification-box").append(element);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The MozChatConversationInfo widget displays information about a chat:
|
||||
|
@ -29,24 +43,54 @@ class MozChatConversationInfo extends MozXULElement {
|
|||
if (this.hasChildNodes() || this.delayConnectedCallback()) {
|
||||
return;
|
||||
}
|
||||
this.setAttribute("orient", "vertical");
|
||||
|
||||
this.appendChild(MozXULElement.parseXULToFragment(`
|
||||
<stack class="statusImageStack">
|
||||
<box class="userIconHolder">
|
||||
<image class="userIcon" mousethrough="always"></image>
|
||||
</box>
|
||||
<image class="statusTypeIcon"></image>
|
||||
</stack>
|
||||
<stack class="displayNameAndstatusMessageStack" mousethrough="always" flex="1">
|
||||
<hbox align="center" flex="1">
|
||||
<description class="displayName" flex="1" crop="end">
|
||||
<hbox class="displayUserAccount" flex="1">
|
||||
<stack class="statusImageStack">
|
||||
<box class="userIconHolder">
|
||||
<image class="userIcon" mousethrough="always"></image>
|
||||
</box>
|
||||
<image class="statusTypeIcon"></image>
|
||||
</stack>
|
||||
<stack class="displayNameAndstatusMessageStack" mousethrough="always" flex="1">
|
||||
<hbox align="center" flex="1">
|
||||
<description class="displayName" flex="1" crop="end">
|
||||
</description>
|
||||
<image class="prplIcon"></image>
|
||||
</hbox>
|
||||
<description class="statusMessage" mousethrough="never" crop="end" flex="100000">
|
||||
</description>
|
||||
<image class="prplIcon"></image>
|
||||
</hbox>
|
||||
<description class="statusMessage" mousethrough="never" crop="end" flex="100000">
|
||||
</description>
|
||||
</stack>
|
||||
`));
|
||||
</stack>
|
||||
</hbox>
|
||||
<hbox class="otr-container" align="left" valign="middle" flex="1" hidden="true">
|
||||
<label class="otr-label" crop="end" value="&state.label;" flex="1"/>
|
||||
<toolbarbutton id="otrButton"
|
||||
mode="dialog"
|
||||
class="otr-button toolbarbutton-1"
|
||||
type="menu"
|
||||
label="Insecure"
|
||||
tooltiptext="&start.label;">
|
||||
<menupopup class="otr-menu-popup">
|
||||
<menuitem class="otr-start" label="&start.label;"
|
||||
oncommand='this.closest("chat-conversation-info").onOtrStartClicked();'/>
|
||||
<menuitem class="otr-end" label="&end.label;"
|
||||
oncommand='this.closest("chat-conversation-info").onOtrEndClicked();'/>
|
||||
<menuitem class="otr-auth" label="&auth.label;"
|
||||
oncommand='this.closest("chat-conversation-info").onOtrAuthClicked();'/>
|
||||
</menupopup>
|
||||
</toolbarbutton>
|
||||
</hbox>
|
||||
<hbox id="otr-notification-box"></hbox>
|
||||
`, ["chrome://chat/content/otr-chat.dtd"]));
|
||||
|
||||
this.topic.addEventListener("click", this.startEditTopic.bind(this));
|
||||
|
||||
if (Services.prefs.getBoolPref("chat.otr.enable")) {
|
||||
let otrButton = this.querySelector(".otr-button");
|
||||
otrButton.addEventListener("command", this.otrButtonClicked);
|
||||
OTRUI.setNotificationBox(gNotification.notificationbox);
|
||||
}
|
||||
this.initializeAttributeInheritance();
|
||||
}
|
||||
|
||||
|
@ -116,5 +160,40 @@ class MozChatConversationInfo extends MozXULElement {
|
|||
}
|
||||
elt.select();
|
||||
}
|
||||
|
||||
otrButtonClicked(aEvent) {
|
||||
aEvent.preventDefault();
|
||||
let otrMenu = this.querySelector(".otr-menu-popup");
|
||||
otrMenu.openPopup(otrMenu.parentNode, "after_start");
|
||||
}
|
||||
|
||||
onOtrStartClicked() {
|
||||
// check if start-menu-command is disabled, if yes exit
|
||||
let convBinding = document.getElementById("conversationsDeck").selectedPanel;
|
||||
let uiConv = convBinding._conv;
|
||||
let conv = uiConv.target;
|
||||
let context = OTR.getContext(conv);
|
||||
let bundleId = "alert." + (
|
||||
context.msgstate === OTR.getMessageState().OTRL_MSGSTATE_ENCRYPTED ?
|
||||
"refresh" : "start");
|
||||
OTRUI.sendSystemAlert(uiConv, conv, bundleId);
|
||||
OTR.sendQueryMsg(conv);
|
||||
}
|
||||
|
||||
onOtrEndClicked() {
|
||||
let convBinding = document.getElementById("conversationsDeck").selectedPanel;
|
||||
let uiConv = convBinding._conv;
|
||||
let conv = uiConv.target;
|
||||
OTR.disconnect(conv, false);
|
||||
let bundleId = "alert.gone_insecure";
|
||||
OTRUI.sendSystemAlert(uiConv, conv, bundleId);
|
||||
}
|
||||
|
||||
onOtrAuthClicked() {
|
||||
let convBinding = document.getElementById("conversationsDeck").selectedPanel;
|
||||
let uiConv = convBinding._conv;
|
||||
let conv = uiConv.target;
|
||||
OTRUI.openAuth(window, conv.normalizedName, "start", uiConv);
|
||||
}
|
||||
}
|
||||
customElements.define("chat-conversation-info", MozChatConversationInfo);
|
||||
|
|
|
@ -93,8 +93,7 @@
|
|||
<toolbarset id="customChatToolbars" context="chat-toolbar-context-menu"/>
|
||||
</toolbox>
|
||||
|
||||
<vbox id="chat-notification-top" flex="1">
|
||||
<!-- notificationbox will be added here lazily. -->
|
||||
<vbox flex="1">
|
||||
<hbox id="chatPanel" flex="1">
|
||||
<vbox id="listPaneBox" minwidth="125" width="200" persist="width">
|
||||
<richlistbox id="contactlistbox"
|
||||
|
@ -112,59 +111,65 @@
|
|||
</richlistbox>
|
||||
</vbox>
|
||||
<splitter id="listSplitter" collapse="before"/>
|
||||
<deck id="conversationsDeck" flex="1">
|
||||
<vbox flex="1" id="noConvScreen" class="im-placeholder-screen" align="center" pack="center">
|
||||
<hbox id="noConvBox" class="im-placeholder-box" align="top">
|
||||
<vbox id="noConvInnerBox" class="im-placeholder-innerbox" flex="1">
|
||||
<label id="noConvTitle" class="im-placeholder-title">&chat.noConv.title;</label>
|
||||
<description id="noConvDesc"
|
||||
class="im-placeholder-desc">&chat.noConv.description;</description>
|
||||
</vbox>
|
||||
<vbox id="noAccountInnerBox" class="im-placeholder-innerbox" flex="1" hidden="true">
|
||||
<label id="noAccountTitle" class="im-placeholder-title">&chat.noAccount.title;</label>
|
||||
<description id="noAccountDesc"
|
||||
class="im-placeholder-desc">&chat.noAccount.description;</description>
|
||||
<hbox class="im-placeholder-button-box" flex="1">
|
||||
<spacer flex="1"/>
|
||||
<button id="openIMAccountWizardButton" label="&chat.accountWizard.button;"
|
||||
oncommand="openIMAccountWizard();"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
<vbox id="noConnectedAccountInnerBox" class="im-placeholder-innerbox" flex="1" hidden="true">
|
||||
<label id="noConnectedAccountTitle"
|
||||
class="im-placeholder-title">&chat.noConnectedAccount.title;</label>
|
||||
<description id="noConnectedAccountDesc"
|
||||
class="im-placeholder-desc">&chat.noConnectedAccount.description;</description>
|
||||
<hbox class="im-placeholder-button-box" flex="1">
|
||||
<spacer flex="1"/>
|
||||
<button id="openIMAccountManagerButton" label="&chat.showAccountManager.button;"
|
||||
oncommand="openIMAccountMgr();"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
</hbox>
|
||||
</vbox>
|
||||
<vbox id="logDisplay" flex="1">
|
||||
<deck id="logDisplayDeck" flex="1">
|
||||
<vbox flex="1" id="noPreviousConvScreen" class="im-placeholder-screen" align="center" pack="center">
|
||||
<hbox id="noPreviousConvBox" class="im-placeholder-box" align="top">
|
||||
<vbox id="noPreviousConvInnerBox" class="im-placeholder-innerbox" flex="1">
|
||||
<description id="noPreviousConvDesc"
|
||||
class="im-placeholder-desc">&chat.noPreviousConv.description;</description>
|
||||
</vbox>
|
||||
</hbox>
|
||||
</vbox>
|
||||
<vbox flex="1" id="logDisplayBrowserBox">
|
||||
<browser id="conv-log-browser" is="conversation-browser" type="content"
|
||||
contextmenu="chatConversationContextMenu" flex="1"
|
||||
tooltip="imTooltip"/>
|
||||
<html:progress id="log-browserProgress" max="100" hidden="true"/>
|
||||
<findbar id="log-findbar" browserid="conv-log-browser"/>
|
||||
</vbox>
|
||||
</deck>
|
||||
<button id="goToConversation" hidden="true"
|
||||
oncommand="chatHandler.showCurrentConversation();"/>
|
||||
</vbox>
|
||||
</deck>
|
||||
<vbox id="chat-notification-top" flex="1">
|
||||
<!-- notificationbox will be added here lazily. -->
|
||||
<deck id="conversationsDeck" flex="1">
|
||||
|
||||
<vbox flex="1" id="noConvScreen" class="im-placeholder-screen" align="center" pack="center">
|
||||
<hbox id="noConvBox" class="im-placeholder-box" align="top">
|
||||
<vbox id="noConvInnerBox" class="im-placeholder-innerbox" flex="1">
|
||||
<label id="noConvTitle" class="im-placeholder-title">&chat.noConv.title;</label>
|
||||
<description id="noConvDesc"
|
||||
class="im-placeholder-desc">&chat.noConv.description;</description>
|
||||
</vbox>
|
||||
<vbox id="noAccountInnerBox" class="im-placeholder-innerbox" flex="1" hidden="true">
|
||||
<label id="noAccountTitle" class="im-placeholder-title">&chat.noAccount.title;</label>
|
||||
<description id="noAccountDesc"
|
||||
class="im-placeholder-desc">&chat.noAccount.description;</description>
|
||||
<hbox class="im-placeholder-button-box" flex="1">
|
||||
<spacer flex="1"/>
|
||||
<button id="openIMAccountWizardButton" label="&chat.accountWizard.button;"
|
||||
oncommand="openIMAccountWizard();"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
<vbox id="noConnectedAccountInnerBox" class="im-placeholder-innerbox" flex="1" hidden="true">
|
||||
<label id="noConnectedAccountTitle"
|
||||
class="im-placeholder-title">&chat.noConnectedAccount.title;</label>
|
||||
<description id="noConnectedAccountDesc"
|
||||
class="im-placeholder-desc">&chat.noConnectedAccount.description;</description>
|
||||
<hbox class="im-placeholder-button-box" flex="1">
|
||||
<spacer flex="1"/>
|
||||
<button id="openIMAccountManagerButton" label="&chat.showAccountManager.button;"
|
||||
oncommand="openIMAccountMgr();"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
</hbox>
|
||||
</vbox>
|
||||
|
||||
<vbox id="logDisplay" flex="1">
|
||||
<deck id="logDisplayDeck" flex="1">
|
||||
<vbox flex="1" id="noPreviousConvScreen" class="im-placeholder-screen" align="center" pack="center">
|
||||
<hbox id="noPreviousConvBox" class="im-placeholder-box" align="top">
|
||||
<vbox id="noPreviousConvInnerBox" class="im-placeholder-innerbox" flex="1">
|
||||
<description id="noPreviousConvDesc"
|
||||
class="im-placeholder-desc">&chat.noPreviousConv.description;</description>
|
||||
</vbox>
|
||||
</hbox>
|
||||
</vbox>
|
||||
<vbox flex="1" id="logDisplayBrowserBox">
|
||||
<browser id="conv-log-browser" is="conversation-browser" type="content"
|
||||
contextmenu="chatConversationContextMenu" flex="1"
|
||||
tooltip="imTooltip"/>
|
||||
<html:progress id="log-browserProgress" max="100" hidden="true"/>
|
||||
<findbar id="log-findbar" browserid="conv-log-browser"/>
|
||||
</vbox>
|
||||
</deck>
|
||||
<button id="goToConversation" hidden="true"
|
||||
oncommand="chatHandler.showCurrentConversation();"/>
|
||||
</vbox>
|
||||
|
||||
</deck>
|
||||
</vbox>
|
||||
<splitter id="contextSplitter" hidden="true" collapse="after"/>
|
||||
<vbox id="contextPane" hidden="true" width="250" persist="width">
|
||||
<chat-conversation-info id="conv-top-info" class="conv-top-info"/>
|
||||
|
|
|
@ -13,7 +13,9 @@ var { Services: imServices } = ChromeUtils.import("resource:///modules/imService
|
|||
var {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
|
||||
|
||||
ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "OTRUI", "resource:///modules/OTRUI.jsm");
|
||||
|
||||
var gOtrEnabled = false;
|
||||
var gBuddyListContextMenu = null;
|
||||
|
||||
function buddyListContextMenu(aXulMenu) {
|
||||
|
@ -35,7 +37,12 @@ function buddyListContextMenu(aXulMenu) {
|
|||
document.getElementById("context-close-conversation").hidden = !this.onConv;
|
||||
document.getElementById("context-openconversation").disabled =
|
||||
!hide && !this.target.canOpenConversation();
|
||||
|
||||
if (gOtrEnabled) {
|
||||
OTRUI.addBuddyContextMenu(this.menu, document);
|
||||
}
|
||||
}
|
||||
|
||||
buddyListContextMenu.prototype = {
|
||||
openConversation() {
|
||||
if (this.onContact || this.onConv)
|
||||
|
@ -629,6 +636,9 @@ var chatHandler = {
|
|||
document.getElementById("noConvScreen");
|
||||
this.updateTitle();
|
||||
this.observedContact = null;
|
||||
if (gOtrEnabled) {
|
||||
OTRUI.hideOTRButton();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -648,6 +658,9 @@ var chatHandler = {
|
|||
cti.removeAttribute("topicEditable");
|
||||
cti.removeAttribute("noTopic");
|
||||
this.observedContact = null;
|
||||
if (gOtrEnabled) {
|
||||
OTRUI.hideOTRButton();
|
||||
}
|
||||
|
||||
let path = "logs/" + item.log.path;
|
||||
path = OS.Path.join(OS.Constants.Path.profileDir, ...path.split("/"));
|
||||
|
@ -680,6 +693,10 @@ var chatHandler = {
|
|||
item.convView.updateConvStatus();
|
||||
item.update();
|
||||
|
||||
if (gOtrEnabled) {
|
||||
OTRUI.updateOTRButton(item.conv);
|
||||
}
|
||||
|
||||
imServices.logs.getLogsForConversation(item.conv, true).then(aLogs => {
|
||||
if (contactlistbox.selectedItem != item)
|
||||
return;
|
||||
|
@ -700,6 +717,9 @@ var chatHandler = {
|
|||
button.disabled = false;
|
||||
this.observedContact = null;
|
||||
} else if (item.localName == "richlistitem" && item.getAttribute("is") == "chat-contact") {
|
||||
if (gOtrEnabled) {
|
||||
OTRUI.hideOTRButton();
|
||||
}
|
||||
let contact = item.contact;
|
||||
if (this.observedContact && contact &&
|
||||
this.observedContact.id == contact.id) {
|
||||
|
@ -1214,6 +1234,28 @@ var chatHandler = {
|
|||
this.ChatCore.init();
|
||||
this._addObserver("chat-core-initialized");
|
||||
}
|
||||
|
||||
gOtrEnabled =
|
||||
Services.prefs.getBoolPref("chat.otr.enable");
|
||||
|
||||
if (gOtrEnabled) {
|
||||
new Promise(resolve => {
|
||||
if (Services.core.initialized) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
function initObserver() {
|
||||
Services.obs.removeObserver(initObserver, "prpl-init");
|
||||
resolve();
|
||||
}
|
||||
Services.obs.addObserver(initObserver, "prpl-init");
|
||||
}).then(() => {
|
||||
let sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService);
|
||||
let uri = Services.io.newURI("chrome://chat/skin/otr.css");
|
||||
sss.loadAndRegisterSheet(uri, sss.USER_SHEET);
|
||||
OTRUI.init();
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -292,7 +292,6 @@ richlistitem[is="chat-imconv"]:not(:hover) > .closeConversationButton {
|
|||
|
||||
.conv-top-info {
|
||||
margin: 0;
|
||||
padding: 0.6ex;
|
||||
border-style: none;
|
||||
-moz-appearance: none;
|
||||
-moz-window-dragging: no-drag;
|
||||
|
@ -480,6 +479,11 @@ richlistitem[state="disconnected"] .accountStateIcon {
|
|||
}
|
||||
|
||||
/* corresponds to im/themes/conversation.css @media all and (min-height: 251px) */
|
||||
.displayUserAccount {
|
||||
padding: 4px;
|
||||
background-color: -moz-OddTreeRow;
|
||||
}
|
||||
|
||||
.statusImageStack,
|
||||
.displayNameAndstatusMessageStack {
|
||||
margin: 2px 2px;
|
||||
|
|
Загрузка…
Ссылка в новой задаче