Update safety number change warning dialog

This commit is contained in:
Josh Perez 2020-06-25 20:08:58 -04:00 committed by Scott Nonnenberg
parent e87a0103cc
commit 5b83485c89
38 changed files with 1221 additions and 425 deletions

View file

@ -52,6 +52,28 @@ addDecorator((storyFn /* , context */) => {
const secondPaneDeviceTheme = makeDeviceThemeKnob('Second');
const secondPaneMode = makeModeKnob('Second');
// Adding it to the body as well so that we can cover modals and other
// components that are rendered outside of this decorator container
if (firstPaneTheme === '') {
document.body.classList.remove('dark-theme');
} else {
document.body.classList.add('dark-theme');
}
if (firstPaneDeviceTheme === '') {
document.body.classList.remove('ios-theme');
} else {
document.body.classList.add('ios-theme');
}
if (firstPaneMode === 'mouse-mode') {
document.body.classList.remove('keyboard-mode');
document.body.classList.add('mouse-mode');
} else {
document.body.classList.remove('mouse-mode');
document.body.classList.add('keyboard-mode');
}
return (
<div className={styles.container}>
<ClassyProvider themes={['dark']}>

View file

@ -345,20 +345,6 @@
"message": "Your safety numbers with these group members have changed since you last verified. Click a group member to see your new safety number with them.",
"description": "When there are multiple previously-verified group members with safety number changes, a banner will be shown. The list of contacts with safety number changes is shown, and this text introduces that list."
},
"changedSinceVerifiedMultiple": {
"message": "Your safety numbers with multiple group members have changed since you last verified. This could mean that someone is trying to intercept your communication or that they have simply reinstalled Signal.",
"description": "Shown on confirmation dialog when user attempts to send a message"
},
"changedSinceVerified": {
"message": "Your safety number with $name$ has changed since you last verified. This could mean that someone is trying to intercept your communication or that $name$ has simply reinstalled Signal.",
"description": "Shown on confirmation dialog when user attempts to send a message",
"placeholders": {
"name": {
"content": "$1",
"example": "Bob"
}
}
},
"changedRightAfterVerify": {
"message": "The safety number you are trying to verify has changed. Please review your new safety number with $name$. Remember, this change could mean that someone is trying to intercept your communication or that $name$ has simply reinstalled Signal.",
"description": "Shown on the safety number screen when the user has selected to verify/unverify a contact's safety number, and we immediately discover a safety number change",
@ -369,20 +355,10 @@
}
}
},
"changedRecentlyMultiple": {
"message": "Your safety numbers with multiple group members have changed recently. This could mean that someone is trying to intercept your communication or that they have simply reinstalled Signal.",
"changedVerificationWarning": {
"message": "The following people may have reinstalled or changed devices. Verify your safety number with them to ensure privacy.",
"description": "Shown on confirmation dialog when user attempts to send a message"
},
"changedRecently": {
"message": "Your safety number with $name$ has changed recently. This could mean that someone is trying to intercept your communication or that $name$ has simply reinstalled Signal.",
"description": "Shown on confirmation dialog when user attempts to send a message",
"placeholders": {
"name": {
"content": "$1",
"example": "Bob"
}
}
},
"identityKeyErrorOnSend": {
"message": "Your safety number with $name$ has changed. This could either mean that someone is trying to intercept your communication or that $name$ has simply reinstalled Signal. You may wish to verify your safety number with this contact.",
"description": "Shown when user clicks on a failed recipient in the message detail view after an identity key change",
@ -1579,6 +1555,10 @@
"message": "Safety Number has changed",
"description": "A notification shown in the conversation when a contact reinstalls"
},
"safetyNumberChanges": {
"message": "Safety Number Changes",
"description": "Title for safety number changed modal"
},
"safetyNumberChangedGroup": {
"message": "Safety Number with $name$ has changed",
"description": "A notification shown in a group conversation when a contact reinstalls, showing the contact name",

View file

@ -123,6 +123,10 @@
</div>
</script>
<script type='text/x-tmpl-mustache' id='safety-number-change-dialog'>
<div class='safety-number-change-dialog-wrapper'></div>
</script>
<script type='text/x-tmpl-mustache' id='identicon-svg'>
<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'>
<circle cx='50' cy='50' r='40' fill='{{ color }}' />
@ -156,33 +160,8 @@
</script>
<script type='text/x-tmpl-mustache' id='key-verification'>
<div class='container' tabindex='0'>
{{ ^hasTheirKey }}
<div class='placeholder'>{{ theirKeyUnknown }}</div>
{{ /hasTheirKey }}
{{ #hasTheirKey }}
<label> {{ yourSafetyNumberWith }} </label>
<!--<div class='qr'></div>-->
<div class='key'>
{{ #chunks }} <span>{{ . }}</span> {{ /chunks }}
</div>
{{ /hasTheirKey }}
{{ verifyHelp }}
<p> {{> link_to_support }} </p>
<div class='summary'>
{{ #isVerified }}
<span class='icon verified'></span>
{{ /isVerified }}
{{ ^isVerified }}
<span class='icon shield'></span>
{{ /isVerified }}
{{ verifiedStatus }}
</div>
<div class='verify'>
<button class='verify grey'>
{{ verifyButton }}
</button>
</div>
<div class="container" tabindex="0">
<div class="key-verification-wrapper"></div>
</div>
</script>
@ -387,6 +366,7 @@
<script type='text/javascript' src='js/views/install_view.js'></script>
<script type='text/javascript' src='js/views/banner_view.js'></script>
<script type="text/javascript" src="js/views/phone-input-view.js"></script>
<script type='text/javascript' src='js/views/safety_number_change_dialog_view.js'></script>
<script type='text/javascript' src='js/views/standalone_registration_view.js'></script>
<script type='text/javascript' src='js/views/app_view.js'></script>
<script type='text/javascript' src='js/views/clear_data_view.js'></script>

View file

@ -451,9 +451,12 @@
const result = {
id: this.id,
uuid: this.get('uuid'),
e164: this.get('e164'),
isArchived: this.get('isArchived'),
isBlocked: this.isBlocked(),
isVerified: this.isVerified(),
activeAt: this.get('active_at'),
avatarPath: this.getAvatarPath(),
color,

View file

@ -40,6 +40,9 @@ const {
MessageDetail,
} = require('../../ts/components/conversation/MessageDetail');
const { Quote } = require('../../ts/components/conversation/Quote');
const {
SafetyNumberChangeDialog,
} = require('../../ts/components/SafetyNumberChangeDialog');
const {
StagedLinkPreview,
} = require('../../ts/components/conversation/StagedLinkPreview');
@ -51,6 +54,9 @@ const {
} = require('../../ts/state/roots/createCompositionArea');
const { createCallManager } = require('../../ts/state/roots/createCallManager');
const { createLeftPane } = require('../../ts/state/roots/createLeftPane');
const {
createSafetyNumberViewer,
} = require('../../ts/state/roots/createSafetyNumberViewer');
const {
createStickerManager,
} = require('../../ts/state/roots/createStickerManager');
@ -274,6 +280,7 @@ exports.setup = (options = {}) => {
MediaGallery,
MessageDetail,
Quote,
SafetyNumberChangeDialog,
StagedLinkPreview,
Types: {
Message: MediaGalleryMessage,
@ -284,6 +291,7 @@ exports.setup = (options = {}) => {
createCallManager,
createCompositionArea,
createLeftPane,
createSafetyNumberViewer,
createShortcutGuideModal,
createStickerManager,
createStickerPreviewModal,

View file

@ -2502,33 +2502,17 @@
showSendAnywayDialog(contacts) {
return new Promise(resolve => {
let message;
const isUnverified = this.model.isUnverified();
if (contacts.length > 1) {
if (isUnverified) {
message = i18n('changedSinceVerifiedMultiple');
} else {
message = i18n('changedRecentlyMultiple');
}
} else {
const contactName = contacts.at(0).getTitle();
if (isUnverified) {
message = i18n('changedSinceVerified', [contactName, contactName]);
} else {
message = i18n('changedRecently', [contactName, contactName]);
}
}
const dialog = new Whisper.ConfirmationDialogView({
message,
okText: i18n('sendAnyway'),
resolve: () => resolve(true),
reject: () => resolve(false),
const dialog = new Whisper.SafetyNumberChangeDialogView({
contacts,
reject: () => {
resolve(false);
},
resolve: () => {
resolve(true);
},
});
this.$el.prepend(dialog.el);
dialog.focusCancel();
});
},

View file

@ -1,4 +1,4 @@
/* global Whisper, textsecure, QRCode, dcodeIO, libsignal, i18n, _ */
/* global Whisper, Signal */
/* eslint-disable more/no-then */
@ -9,152 +9,18 @@
window.Whisper = window.Whisper || {};
Whisper.KeyVerificationPanelView = Whisper.View.extend({
className: 'key-verification panel',
className: 'panel',
templateName: 'key-verification',
events: {
'click button.verify': 'toggleVerified',
},
initialize(options) {
this.ourNumber = textsecure.storage.user.getNumber();
this.ourUuid = textsecure.storage.user.getUuid();
if (options.newKey) {
this.theirKey = options.newKey;
}
this.loadTheirKey();
this.loadOurKey();
this.render();
if (options.onLoad) {
options.onLoad(this);
}
this.loadKeys().then(() => {
this.listenTo(this.model, 'change', this.render);
});
},
async loadKeys() {
await this.generateSecurityNumber();
this.render();
},
makeQRCode() {
// Per Lilia: We can't turn this on until it generates a Latin1 string, as is
// required by the mobile clients.
new QRCode(this.$('.qr')[0]).makeCode(
dcodeIO.ByteBuffer.wrap(this.ourKey).toString('base64')
);
},
loadTheirKey() {
const item = textsecure.storage.protocol.getIdentityRecord(
this.model.get('id')
);
this.theirKey = item ? item.publicKey : null;
},
loadOurKey() {
const item = textsecure.storage.protocol.getIdentityRecord(
this.ourUuid || this.ourNumber
);
this.ourKey = item ? item.publicKey : null;
},
generateSecurityNumber() {
return new libsignal.FingerprintGenerator(5200)
.createFor(
// TODO: we cannot use UUIDs for safety numbers yet
// this.ourUuid || this.ourNumber,
this.ourNumber,
this.ourKey,
// TODO: we cannot use UUIDs for safety numbers yet
// this.model.get('uuid') || this.model.get('e164'),
this.model.get('e164'),
this.theirKey
)
.then(securityNumber => {
this.securityNumber = securityNumber;
});
},
onSafetyNumberChanged() {
this.model.getProfiles().then(this.loadKeys.bind(this));
const dialog = new Whisper.ConfirmationDialogView({
message: i18n('changedRightAfterVerify', [
this.model.getTitle(),
this.model.getTitle(),
]),
hideCancel: true,
const view = new Whisper.ReactWrapperView({
JSX: Signal.State.Roots.createSafetyNumberViewer(window.reduxStore, {
contactID: options.model.get('id'),
}),
});
dialog.$el.insertBefore(this.el);
dialog.focusCancel();
},
toggleVerified() {
this.$('button.verify').attr('disabled', true);
this.model
.toggleVerified()
.catch(result => {
if (result instanceof Error) {
if (result.name === 'OutgoingIdentityKeyError') {
this.onSafetyNumberChanged();
} else {
window.log.error(
'failed to toggle verified:',
result && result.stack ? result.stack : result
);
}
} else {
const keyError = _.some(
result.errors,
error => error.name === 'OutgoingIdentityKeyError'
);
if (keyError) {
this.onSafetyNumberChanged();
} else {
_.forEach(result.errors, error => {
window.log.error(
'failed to toggle verified:',
error && error.stack ? error.stack : error
);
});
}
}
})
.then(() => {
this.$('button.verify').removeAttr('disabled');
});
},
render_attributes() {
const s = this.securityNumber;
const chunks = [];
if (s) {
for (let i = 0; i < s.length; i += 5) {
chunks.push(s.substring(i, i + 5));
}
} else {
for (let i = 0; i < 12; i += 1) {
chunks.push('XXXXX');
}
}
const name = this.model.getTitle();
const yourSafetyNumberWith = i18n(
'yourSafetyNumberWith',
this.model.getTitle()
);
const isVerified = this.model.isVerified();
const verifyButton = isVerified ? i18n('unverify') : i18n('verify');
const verifiedStatus = isVerified
? i18n('isVerified', name)
: i18n('isNotVerified', name);
return {
learnMore: i18n('learnMore'),
theirKeyUnknown: i18n('theirIdentityUnknown'),
yourSafetyNumberWith,
verifyHelp: i18n('verifyHelp', this.model.getTitle()),
verifyButton,
hasTheirKey: this.theirKey !== undefined,
chunks,
isVerified,
verifiedStatus,
};
this.$('.key-verification-wrapper').append(view.el);
},
});
})();

View file

@ -0,0 +1,39 @@
/* global Whisper, Signal */
// eslint-disable-next-line func-names
(function() {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.SafetyNumberChangeDialogView = Whisper.View.extend({
templateName: 'safety-number-change-dialog',
initialize(options) {
const dialog = new Whisper.ReactWrapperView({
Component: window.Signal.Components.SafetyNumberChangeDialog,
props: {
contacts: options.contacts.map(contact => contact.cachedProps),
i18n: window.i18n,
onCancel: () => {
dialog.remove();
this.remove();
options.reject();
},
onConfirm: () => {
dialog.remove();
this.remove();
options.resolve();
},
renderSafetyNumber(props) {
return Signal.State.Roots.createSafetyNumberViewer(
window.reduxStore,
props
);
},
},
});
this.$('.safety-number-change-dialog-wrapper').append(dialog.el);
},
});
})();

View file

@ -118,105 +118,6 @@
}
}
.key-verification {
.container {
outline: none;
}
label {
display: block;
margin: 10px 0;
}
.icon {
height: 1.25em;
width: 1.25em;
vertical-align: text-bottom;
display: inline-block;
&.verified {
@include light-theme {
@include color-svg('../images/icons/v2/check-24.svg', $color-gray-95);
}
@include dark-theme {
@include color-svg('../images/icons/v2/check-24.svg', $color-gray-02);
}
}
&.shield {
@include light-theme {
@include color-svg(
'../images/icons/v2/safety-number-outline-24.svg',
$color-gray-95
);
}
@include dark-theme {
@include color-svg(
'../images/icons/v2/safety-number-solid-24.svg',
$color-gray-02
);
}
}
}
.key,
.placeholder {
padding: 0 1em;
-webkit-user-select: text;
}
.key {
font-family: monospace;
padding: 10px;
margin: 20px auto 20px auto;
width: 16em;
border-radius: 5px;
@include light-theme {
background: $color-gray-02;
border: solid 1px $color-gray-15;
}
@include dark-theme {
color: $color-gray-02;
background: $color-gray-90;
border: solid 1px $color-gray-45;
}
}
.placeholder {
font-weight: bold;
}
.qr {
border-radius: 200px;
border: solid 1px $color-gray-15;
width: 150px;
height: 150px;
text-align: center;
padding: 25px;
margin: 10px auto;
canvas {
display: none;
}
img {
display: inline-block;
max-width: 100%;
}
}
.summary {
margin: 30px 0 10px;
text-align: center;
}
div.verify {
text-align: center;
}
button.verify {
border-radius: 5px;
font-weight: bold;
padding: 10px;
margin: 0;
}
}
.typing-bubble-wrapper {
margin-bottom: 20px;
}

View file

@ -6139,6 +6139,279 @@ button.module-image__border-overlay:focus {
}
}
// Module: SafetyNumberChangeDialog
.module-sfn-dialog__title {
@include font-body-1-bold;
text-align: center;
@include dark-theme {
color: $color-white;
}
}
.module-sfn-dialog__message {
@include font-body-2;
text-align: center;
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}
.module-sfn-dialog__contacts {
list-style-type: none;
max-height: 300px;
overflow-y: scroll;
padding: 0;
}
.module-sfn-dialog__contact {
align-items: center;
display: flex;
flex-direction: row;
margin-bottom: 16px;
&--wrapper {
flex-grow: 1;
margin-left: 12px;
}
&--name {
@include font-body-1-bold;
@include dark-theme {
color: $color-white;
}
}
&--number {
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}
&--view {
@include font-body-1-bold;
background: inherit;
border: none;
cursor: pointer;
margin-right: 2px;
outline: none;
padding: 8px 14px;
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
}
}
@include light-theme {
color: $ultramarine-ui-light;
}
@include dark-theme {
color: $ultramarine-ui-dark;
}
}
}
.module-sfn-dialog__actions {
border-top: 1px solid $color-gray-05;
display: flex;
justify-content: flex-end;
margin-bottom: -18px;
margin-left: -16px;
margin-right: -16px;
margin-top: -14px;
padding-left: 16px;
padding-right: 16px;
padding-top: 16px;
&--cancel {
@include font-body-1-bold;
border: none;
border-radius: 4px;
outline: none;
padding: 7px 14px;
@include mouse-mode {
&:hover {
background: $color-gray-15;
}
}
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
}
}
@include light-theme {
background-color: $color-gray-05;
color: $ultramarine-ui-light;
}
@include dark-theme {
background-color: $color-gray-75;
color: $ultramarine-ui-dark;
}
}
&--confirm {
@include font-body-1-bold;
background: $ultramarine-ui-light;
border: none;
border-radius: 4px;
color: $color-white;
margin-left: 12px;
outline: none;
padding: 7px 14px;
@include mouse-mode {
&:hover {
background: $ultramarine-brand-dark;
}
}
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $ultramarine-brand-dark;
}
}
}
}
/* Safety Number verification */
.module-safety-number {
&__icon {
height: 1.25em;
width: 1.25em;
vertical-align: text-bottom;
display: inline-block;
}
&__verification-label {
margin: 10px 0;
}
&__icon--verified {
display: inline-block;
height: 1.25em;
margin-right: 4px;
vertical-align: text-bottom;
width: 1.25em;
@include light-theme {
-webkit-mask: url('../images/icons/v2/check-24.svg') no-repeat center;
-webkit-mask-size: 100%;
background-color: #121212;
}
@include dark-theme {
-webkit-mask: url('../images/icons/v2/check-24.svg') no-repeat center;
-webkit-mask-size: 100%;
background-color: #f6f6f6;
}
}
&__icon--shield {
display: inline-block;
height: 1.25em;
margin-right: 4px;
vertical-align: text-bottom;
width: 1.25em;
@include light-theme {
-webkit-mask: url('../images/icons/v2/safety-number-outline-24.svg')
no-repeat center;
-webkit-mask-size: 100%;
background-color: #121212;
}
@include dark-theme {
-webkit-mask: url('../images/icons/v2/safety-number-solid-24.svg')
no-repeat center;
-webkit-mask-size: 100%;
background-color: #f6f6f6;
}
}
&__verify-container {
text-align: center;
}
&__button--verify {
border-radius: 5px;
font-weight: bold;
margin: 0;
outline: none;
padding: 10px;
}
&__number {
background: #f6f6f6;
border-radius: 5px;
border: solid 1px #dedede;
font-family: monospace;
margin: 20px auto 20px auto;
padding: 10px;
text-align: center;
width: 16em;
@include dark-theme {
background: #1b1b1b;
border: solid 1px #848484;
color: #f6f6f6;
}
}
&__verification-status {
margin: 30px 0 10px;
text-align: center;
}
&__close-button {
display: flex;
justify-content: flex-end;
button {
background: inherit;
border: none;
cursor: pointer;
padding: 0;
@include keyboard-mode {
&:focus {
border: 1px solid $ultramarine-ui-light;
}
}
span {
display: inline-block;
height: 24px;
width: 24px;
@include light-theme {
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-60);
}
@include dark-theme {
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-05);
}
}
}
}
}
// Module: StickerPicker
.module-sticker-picker {
@ -7094,7 +7367,7 @@ button.module-image__border-overlay:focus {
}
@include dark-theme() {
background: $color-gray-75;
background: $color-gray-80;
color: $color-gray-05;
}

View file

@ -167,33 +167,8 @@
</script>
<script type='text/x-tmpl-mustache' id='key-verification'>
<div class='container'>
{{ ^hasTheirKey }}
<div class='placeholder'>{{ theirKeyUnknown }}</div>
{{ /hasTheirKey }}
{{ #hasTheirKey }}
<label> {{ yourSafetyNumberWith }} </label>
<!--<div class='qr'></div>-->
<div class='key'>
{{ #chunks }} <span>{{ . }}</span> {{ /chunks }}
</div>
{{ /hasTheirKey }}
{{ verifyHelp }}
<p> {{> link_to_support }} </p>
<div class='summary'>
{{ #isVerified }}
<span class='icon verified'></span>
{{ /isVerified }}
{{ ^isVerified }}
<span class='icon shield'></span>
{{ /isVerified }}
{{ verifiedStatus }}
</div>
<div class='verify'>
<button class='verify grey'>
{{ verifyButton }}
</button>
</div>
<div class="container" tabindex="0">
<div class="key-verification-wrapper"></div>
</div>
</script>

View file

@ -1,4 +1,3 @@
// tslint:disable: no-backbone-get-set-outside-model
import { get, throttle } from 'lodash';
import { WebAPIType } from './textsecure/WebAPI';

View file

@ -70,33 +70,35 @@ export const ConfirmationDialog = React.memo(
<div className="module-confirmation-dialog__container__content">
{children}
</div>
<div className="module-confirmation-dialog__container__buttons">
<button
onClick={handleCancel}
ref={focusRef}
className="module-confirmation-dialog__container__buttons__button"
>
{i18n('confirmation-dialog--Cancel')}
</button>
{actions.map((action, i) => (
{actions.length > 0 && (
<div className="module-confirmation-dialog__container__buttons">
<button
key={i}
onClick={handleAction}
data-action={i}
className={classNames(
'module-confirmation-dialog__container__buttons__button',
action.style === 'affirmative'
? 'module-confirmation-dialog__container__buttons__button--affirmative'
: null,
action.style === 'negative'
? 'module-confirmation-dialog__container__buttons__button--negative'
: null
)}
onClick={handleCancel}
ref={focusRef}
className="module-confirmation-dialog__container__buttons__button"
>
{action.text}
{i18n('confirmation-dialog--Cancel')}
</button>
))}
</div>
{actions.map((action, i) => (
<button
key={i}
onClick={handleAction}
data-action={i}
className={classNames(
'module-confirmation-dialog__container__buttons__button',
action.style === 'affirmative'
? 'module-confirmation-dialog__container__buttons__button--affirmative'
: null,
action.style === 'negative'
? 'module-confirmation-dialog__container__buttons__button--negative'
: null
)}
>
{action.text}
</button>
))}
</div>
)}
</div>
);
}

View file

@ -0,0 +1,76 @@
import * as React from 'react';
import { SafetyNumberChangeDialog } from './SafetyNumberChangeDialog';
import { ConversationType } from '../state/ducks/conversations';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
const i18n = setupI18n('en', enMessages);
const contact = {
avatarPath: undefined,
color: 'signal-blue',
profileName: undefined,
name: 'Rick Sanchez',
phoneNumber: '3051234567',
} as ConversationType;
storiesOf('Components/SafetyNumberChangeDialog', module)
.add('Single Contact Dialog', () => {
return (
<SafetyNumberChangeDialog
contacts={[contact]}
i18n={i18n}
onCancel={action('cancel')}
onConfirm={action('confirm')}
renderSafetyNumber={() => {
action('renderSafetyNumber');
return <div>This is a mock Safety Number View</div>;
}}
/>
);
})
.add('Multi Contact Dialog', () => {
return (
<SafetyNumberChangeDialog
contacts={[contact, contact, contact, contact]}
i18n={i18n}
onCancel={action('cancel')}
onConfirm={action('confirm')}
renderSafetyNumber={() => {
action('renderSafetyNumber');
return <div>This is a mock Safety Number View</div>;
}}
/>
);
})
.add('Scroll Dialog', () => {
return (
<SafetyNumberChangeDialog
contacts={[
contact,
contact,
contact,
contact,
contact,
contact,
contact,
contact,
contact,
contact,
]}
i18n={i18n}
onCancel={action('cancel')}
onConfirm={action('confirm')}
renderSafetyNumber={() => {
action('renderSafetyNumber');
return <div>This is a mock Safety Number View</div>;
}}
/>
);
});

View file

@ -0,0 +1,135 @@
import * as React from 'react';
import { Avatar } from './Avatar';
import { ConfirmationModal } from './ConfirmationModal';
import { ConversationType } from '../state/ducks/conversations';
import { LocalizerType } from '../types/Util';
type SafetyNumberProps = {
contactID: string;
onClose?: () => void;
};
export type Props = {
readonly contacts: Array<ConversationType>;
readonly i18n: LocalizerType;
readonly onCancel: () => void;
readonly onConfirm: () => void;
readonly renderSafetyNumber: (props: SafetyNumberProps) => JSX.Element;
};
type SafetyDialogContentProps = Props & {
readonly onView: (contact: ConversationType) => void;
};
const SafetyDialogContents = ({
contacts,
i18n,
onCancel,
onConfirm,
onView,
}: SafetyDialogContentProps): JSX.Element => {
const cancelButtonRef = React.createRef<HTMLButtonElement>();
React.useEffect(() => {
if (cancelButtonRef && cancelButtonRef.current) {
cancelButtonRef.current.focus();
}
}, [contacts]);
return (
<>
<h1 className="module-sfn-dialog__title">
{i18n('safetyNumberChanges')}
</h1>
<div className="module-sfn-dialog__message">
{i18n('changedVerificationWarning')}
</div>
<ul className="module-sfn-dialog__contacts">
{contacts.map((contact: ConversationType) => (
<li className="module-sfn-dialog__contact" key={contact.phoneNumber}>
<Avatar
avatarPath={contact.avatarPath}
color={contact.color}
conversationType="direct"
i18n={i18n}
name={contact.name}
phoneNumber={contact.phoneNumber}
profileName={contact.profileName}
size={52}
/>
<div className="module-sfn-dialog__contact--wrapper">
{contact.name && (
<>
<div className="module-sfn-dialog__contact--name">
{contact.name}
</div>
<div className="module-sfn-dialog__contact--number">
{contact.phoneNumber}
</div>
</>
)}
{!contact.name && (
<div className="module-sfn-dialog__contact--name">
{contact.phoneNumber}
</div>
)}
</div>
<button
className="module-sfn-dialog__contact--view"
onClick={() => {
onView(contact);
}}
tabIndex={0}
>
{i18n('view')}
</button>
</li>
))}
</ul>
<div className="module-sfn-dialog__actions">
<button
className="module-sfn-dialog__actions--cancel"
onClick={onCancel}
ref={cancelButtonRef}
tabIndex={0}
>
{i18n('cancel')}
</button>
<button
className="module-sfn-dialog__actions--confirm"
onClick={onConfirm}
tabIndex={0}
>
{i18n('sendMessageToContact')}
</button>
</div>
</>
);
};
export const SafetyNumberChangeDialog = (props: Props): JSX.Element => {
const { i18n, onCancel, renderSafetyNumber } = props;
const [contact, setViewSafetyNumber] = React.useState<
ConversationType | undefined
>(undefined);
const onClose = contact
? () => {
setViewSafetyNumber(undefined);
}
: onCancel;
return (
<ConfirmationModal actions={[]} i18n={i18n} onClose={onClose}>
{contact && renderSafetyNumber({ contactID: contact.id, onClose })}
{!contact && (
<SafetyDialogContents
{...props}
onView={selectedContact => {
setViewSafetyNumber(selectedContact);
}}
/>
)}
</ConfirmationModal>
);
};

View file

@ -0,0 +1,85 @@
import * as React from 'react';
import { SafetyNumberViewer } from './SafetyNumberViewer';
import { ConversationType } from '../state/ducks/conversations';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { action } from '@storybook/addon-actions';
import { boolean, text } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react';
const i18n = setupI18n('en', enMessages);
const defaultProps = {
contact: {
title: 'Summer Smith',
isVerified: true,
} as ConversationType,
generateSafetyNumber: action('generate-safety-number'),
i18n,
safetyNumber: 'XXX',
safetyNumberChanged: false,
toggleVerified: action('toggle-verified'),
verificationDisabled: false,
};
const permutations = [
{
title: 'Safety Number',
props: {},
},
{
title: 'Safety Number (not verified)',
props: {
contact: {
title: 'Morty Smith',
isVerified: false,
} as ConversationType,
},
},
{
title: 'Verification Disabled',
props: {
verificationDisabled: true,
},
},
{
title: 'Safety Number Changed',
props: {
safetyNumberChanged: true,
},
},
{
title: 'Safety Number (dialog close)',
props: {
onClose: action('close'),
},
},
];
storiesOf('Components/SafetyNumberViewer', module)
.add('Knobs Playground', () => {
const safetyNumber = text('safetyNumber', 'XXX');
const safetyNumberChanged = boolean('safetyNumberChanged', false);
const verificationDisabled = boolean('verificationDisabled', false);
return (
<SafetyNumberViewer
{...defaultProps}
safetyNumber={safetyNumber}
safetyNumberChanged={safetyNumberChanged}
verificationDisabled={verificationDisabled}
/>
);
})
.add('Iterations', () => {
return permutations.map(({ props, title }) => (
<>
<h3>{title}</h3>
<SafetyNumberViewer {...defaultProps} {...props} />
</>
));
});

View file

@ -0,0 +1,82 @@
import React from 'react';
import { ConversationType } from '../state/ducks/conversations';
import { LocalizerType } from '../types/Util';
import { getPlaceholder } from '../util/safetyNumber';
type SafetyNumberViewerProps = {
contact?: ConversationType;
generateSafetyNumber: (contact: ConversationType) => void;
i18n: LocalizerType;
onClose?: () => void;
safetyNumber: string;
safetyNumberChanged?: boolean;
toggleVerified: (contact: ConversationType) => void;
verificationDisabled: boolean;
};
export const SafetyNumberViewer = ({
contact,
generateSafetyNumber,
i18n,
onClose,
safetyNumber,
safetyNumberChanged,
toggleVerified,
verificationDisabled,
}: SafetyNumberViewerProps): JSX.Element | null => {
if (!contact) {
return null;
}
React.useEffect(() => {
generateSafetyNumber(contact);
}, [safetyNumber]);
const name = contact.title;
const isVerified = contact.isVerified;
const verifiedStatus = isVerified
? i18n('isVerified', [name])
: i18n('isNotVerified', [name]);
const verifyButtonText = isVerified ? i18n('unverify') : i18n('verify');
return (
<div className="module-safety-number">
{onClose && (
<div className="module-safety-number__close-button">
<button onClick={onClose} tabIndex={0}>
<span />
</button>
</div>
)}
<div className="module-safety-number__verification-label">
{safetyNumberChanged
? i18n('changedRightAfterVerify', [name, name])
: i18n('yourSafetyNumberWith', [name])}
</div>
<div className="module-safety-number__number">
{safetyNumber || getPlaceholder()}
</div>
{i18n('verifyHelp', [name])}
<div className="module-safety-number__verification-status">
{isVerified ? (
<span className="module-safety-number__icon--verified" />
) : (
<span className="module-safety-number__icon--shield" />
)}
{verifiedStatus}
</div>
<div className="module-safety-number__verify-container">
<button
className="module-safety-number__button--verify"
disabled={verificationDisabled}
onClick={() => {
toggleVerified(contact);
}}
tabIndex={0}
>
{verifyButtonText}
</button>
</div>
</div>
);
};

View file

@ -37,7 +37,6 @@ const SENT = 'sent' as 'sent';
const START_NEW_CONVERSATION = 'start-new-conversation' as 'start-new-conversation';
const SMS_MMS_NOT_SUPPORTED = 'sms-mms-not-supported-text' as 'sms-mms-not-supported-text';
// tslint:disable-next-line no-backbone-get-set-outside-model
messageLookup.set('1-guid-guid-guid-guid-guid', {
id: '1-guid-guid-guid-guid-guid',
conversationId: '(202) 555-0015',
@ -56,7 +55,6 @@ messageLookup.set('1-guid-guid-guid-guid-guid', {
},
});
// tslint:disable-next-line no-backbone-get-set-outside-model
messageLookup.set('2-guid-guid-guid-guid-guid', {
id: '2-guid-guid-guid-guid-guid',
conversationId: '(202) 555-0016',
@ -73,7 +71,6 @@ messageLookup.set('2-guid-guid-guid-guid-guid', {
},
});
// tslint:disable-next-line no-backbone-get-set-outside-model
messageLookup.set('3-guid-guid-guid-guid-guid', {
id: '3-guid-guid-guid-guid-guid',
conversationId: 'EveryoneGroupID',
@ -91,7 +88,6 @@ messageLookup.set('3-guid-guid-guid-guid-guid', {
},
});
// tslint:disable-next-line no-backbone-get-set-outside-model
messageLookup.set('4-guid-guid-guid-guid-guid', {
id: '4-guid-guid-guid-guid-guid',
conversationId: 'EveryoneGroupID',

View file

@ -0,0 +1,13 @@
export async function toggleVerification(id: string): Promise<void> {
const contact = window.getConversations().get(id);
if (contact) {
await contact.toggleVerified();
}
}
export async function reloadProfiles(id: string): Promise<void> {
const contact = window.getConversations().get(id);
if (contact) {
await contact.getProfiles();
}
}

View file

@ -1,4 +1,4 @@
// tslint:disable no-backbone-get-set-outside-model no-console no-default-export no-unnecessary-local-variable
// tslint:disable no-console no-default-export no-unnecessary-local-variable
import { join } from 'path';
import mkdirp from 'mkdirp';

View file

@ -4,6 +4,7 @@ import { actions as emojis } from './ducks/emojis';
import { actions as expiration } from './ducks/expiration';
import { actions as items } from './ducks/items';
import { actions as network } from './ducks/network';
import { actions as safetyNumber } from './ducks/safetyNumber';
import { actions as search } from './ducks/search';
import { actions as stickers } from './ducks/stickers';
import { actions as updates } from './ducks/updates';
@ -16,6 +17,7 @@ export const mapDispatchToProps = {
...expiration,
...items,
...network,
...safetyNumber,
...search,
...stickers,
...updates,

View file

@ -24,12 +24,15 @@ export type DBConversationType = {
};
export type ConversationType = {
id: string;
uuid?: string;
e164: string;
name?: string;
profileName?: string;
avatarPath?: string;
color?: ColorType;
isArchived?: boolean;
isBlocked?: boolean;
isVerified?: boolean;
activeAt?: number;
timestamp: number;
inboxPosition: number;
@ -42,6 +45,7 @@ export type ConversationType = {
type: 'direct' | 'group';
isMe: boolean;
lastUpdated: number;
title: string;
unreadCount: number;
isSelected: boolean;
typingContact?: {

View file

@ -0,0 +1,214 @@
import { generateSecurityNumberBlock } from '../../util/safetyNumber';
import { ConversationType } from './conversations';
import {
reloadProfiles,
toggleVerification,
} from '../../shims/contactVerification';
export type SafetyNumberContactType = {
safetyNumber: string;
safetyNumberChanged?: boolean;
verificationDisabled: boolean;
};
export type SafetyNumberStateType = {
contacts: {
[key: string]: SafetyNumberContactType;
};
};
const GENERATE = 'safetyNumber/GENERATE';
const GENERATE_FULFILLED = 'safetyNumber/GENERATE_FULFILLED';
const TOGGLE_VERIFIED = 'safetyNumber/TOGGLE_VERIFIED';
const TOGGLE_VERIFIED_FULFILLED = 'safetyNumber/TOGGLE_VERIFIED_FULFILLED';
const TOGGLE_VERIFIED_PENDING = 'safetyNumber/TOGGLE_VERIFIED_PENDING';
type GenerateAsyncActionType = {
contact: ConversationType;
safetyNumber: string;
};
type GenerateActionType = {
type: 'safetyNumber/GENERATE';
payload: Promise<GenerateAsyncActionType>;
};
type GenerateFulfilledActionType = {
type: 'safetyNumber/GENERATE_FULFILLED';
payload: GenerateAsyncActionType;
};
type ToggleVerifiedAsyncActionType = {
contact: ConversationType;
safetyNumber?: string;
safetyNumberChanged?: boolean;
};
type ToggleVerifiedActionType = {
type: 'safetyNumber/TOGGLE_VERIFIED';
payload: {
data: { contact: ConversationType };
promise: Promise<ToggleVerifiedAsyncActionType>;
};
};
type ToggleVerifiedPendingActionType = {
type: 'safetyNumber/TOGGLE_VERIFIED_PENDING';
payload: ToggleVerifiedAsyncActionType;
};
type ToggleVerifiedFulfilledActionType = {
type: 'safetyNumber/TOGGLE_VERIFIED_FULFILLED';
payload: ToggleVerifiedAsyncActionType;
};
export type SafetyNumberActionTypes =
| GenerateActionType
| GenerateFulfilledActionType
| ToggleVerifiedActionType
| ToggleVerifiedPendingActionType
| ToggleVerifiedFulfilledActionType;
function generate(contact: ConversationType): GenerateActionType {
return {
type: GENERATE,
payload: doGenerate(contact),
};
}
async function doGenerate(
contact: ConversationType
): Promise<GenerateAsyncActionType> {
const securityNumberBlock = await generateSecurityNumberBlock(contact);
return {
contact,
safetyNumber: securityNumberBlock.join(' '),
};
}
function toggleVerified(contact: ConversationType): ToggleVerifiedActionType {
return {
type: TOGGLE_VERIFIED,
payload: {
data: { contact },
promise: doToggleVerified(contact),
},
};
}
async function alterVerification(contact: ConversationType): Promise<void> {
try {
await toggleVerification(contact.id);
} catch (result) {
if (result instanceof Error) {
if (result.name === 'OutgoingIdentityKeyError') {
throw result;
} else {
window.log.error(
'failed to toggle verified:',
result && result.stack ? result.stack : result
);
}
} else {
const keyError = result.errors.find(
(error: Error) => error.name === 'OutgoingIdentityKeyError'
);
if (keyError) {
throw keyError;
} else {
result.errors.forEach((error: Error) => {
window.log.error(
'failed to toggle verified:',
error && error.stack ? error.stack : error
);
});
}
}
}
}
async function doToggleVerified(
contact: ConversationType
): Promise<ToggleVerifiedAsyncActionType> {
try {
await alterVerification(contact);
} catch (err) {
if (err.name === 'OutgoingIdentityKeyError') {
await reloadProfiles(contact.id);
const securityNumberBlock = await generateSecurityNumberBlock(contact);
return {
contact,
safetyNumber: securityNumberBlock.join(' '),
safetyNumberChanged: true,
};
}
}
return { contact };
}
export const actions = {
generateSafetyNumber: generate,
toggleVerified,
};
function getEmptyState(): SafetyNumberStateType {
return {
contacts: {},
};
}
export function reducer(
state: SafetyNumberStateType = getEmptyState(),
action: SafetyNumberActionTypes
): SafetyNumberStateType {
if (action.type === TOGGLE_VERIFIED_PENDING) {
const { contact } = action.payload;
const { id } = contact;
const record = state.contacts[id];
return {
contacts: {
...state.contacts,
[id]: {
...record,
safetyNumberChanged: false,
verificationDisabled: true,
},
},
};
}
if (action.type === TOGGLE_VERIFIED_FULFILLED) {
const { contact, ...restProps } = action.payload;
const { id } = contact;
const record = state.contacts[id];
return {
contacts: {
...state.contacts,
[id]: {
...record,
...restProps,
verificationDisabled: false,
},
},
};
}
if (action.type === GENERATE_FULFILLED) {
const { contact, safetyNumber } = action.payload;
const { id } = contact;
const record = state.contacts[id];
return {
contacts: {
...state.contacts,
[id]: {
...record,
safetyNumber,
},
},
};
}
return state;
}

View file

@ -30,6 +30,11 @@ import {
NetworkStateType,
reducer as network,
} from './ducks/network';
import {
reducer as safetyNumber,
SafetyNumberActionTypes,
SafetyNumberStateType,
} from './ducks/safetyNumber';
import {
reducer as search,
SEARCH_TYPES as SearchActionType,
@ -54,6 +59,7 @@ export type StateType = {
expiration: ExpirationStateType;
items: ItemsStateType;
network: NetworkStateType;
safetyNumber: SafetyNumberStateType;
search: SearchStateType;
stickers: StickersStateType;
updates: UpdatesStateType;
@ -67,6 +73,7 @@ export type ActionsType =
| ConversationActionType
| ItemsActionType
| NetworkActionType
| SafetyNumberActionTypes
| StickersActionType
| SearchActionType
| UpdatesActionType;
@ -78,6 +85,7 @@ export const reducers = {
expiration,
items,
network,
safetyNumber,
search,
stickers,
updates,

View file

@ -0,0 +1,21 @@
import React from 'react';
import { Provider } from 'react-redux';
import { Store } from 'redux';
import { SmartSafetyNumberViewer } from '../smart/SafetyNumberViewer';
// Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
const FilteredSafetyNumberViewer = SmartSafetyNumberViewer as any;
type Props = {
contactID: string;
onClose?: () => void;
};
export const createSafetyNumberViewer = (store: Store, props: Props) => (
<Provider store={store}>
<FilteredSafetyNumberViewer {...props} />
</Provider>
);

View file

@ -0,0 +1,24 @@
import { createSelector } from 'reselect';
import { StateType } from '../reducer';
import {
SafetyNumberContactType,
SafetyNumberStateType,
} from '../ducks/safetyNumber';
const getSafetyNumber = (state: StateType): SafetyNumberStateType =>
state.safetyNumber;
type Props = {
contactID: string;
};
const getContactID = (_: StateType, props: Props): string => props.contactID;
export const getContactSafetyNumber = createSelector(
[getSafetyNumber, getContactID],
(
{ contacts }: SafetyNumberStateType,
contactID: string
): SafetyNumberContactType => contacts[contactID]
);

View file

@ -0,0 +1,25 @@
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import { SafetyNumberViewer } from '../../components/SafetyNumberViewer';
import { StateType } from '../reducer';
import { getContactSafetyNumber } from '../selectors/safetyNumber';
import { getConversationSelector } from '../selectors/conversations';
import { getIntl } from '../selectors/user';
type Props = {
contactID: string;
onClose?: () => void;
};
const mapStateToProps = (state: StateType, props: Props) => {
return {
...props,
...getContactSafetyNumber(state, props),
contact: getConversationSelector(state)(props.contactID),
i18n: getIntl(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartSafetyNumberViewer = smart(SafetyNumberViewer);

View file

@ -14,6 +14,7 @@ describe('state/selectors/conversations', () => {
const data: ConversationLookupType = {
id1: {
id: 'id1',
e164: '+18005551111',
activeAt: Date.now(),
name: 'No timestamp',
timestamp: 0,
@ -24,6 +25,7 @@ describe('state/selectors/conversations', () => {
type: 'direct',
isMe: false,
lastUpdated: Date.now(),
title: 'No timestamp',
unreadCount: 1,
isSelected: false,
typingContact: {
@ -36,6 +38,7 @@ describe('state/selectors/conversations', () => {
},
id2: {
id: 'id2',
e164: '+18005551111',
activeAt: Date.now(),
name: 'B',
timestamp: 20,
@ -46,6 +49,7 @@ describe('state/selectors/conversations', () => {
type: 'direct',
isMe: false,
lastUpdated: Date.now(),
title: 'B',
unreadCount: 1,
isSelected: false,
typingContact: {
@ -58,6 +62,7 @@ describe('state/selectors/conversations', () => {
},
id3: {
id: 'id3',
e164: '+18005551111',
activeAt: Date.now(),
name: 'C',
timestamp: 20,
@ -68,6 +73,7 @@ describe('state/selectors/conversations', () => {
type: 'direct',
isMe: false,
lastUpdated: Date.now(),
title: 'C',
unreadCount: 1,
isSelected: false,
typingContact: {
@ -80,6 +86,7 @@ describe('state/selectors/conversations', () => {
},
id4: {
id: 'id4',
e164: '+18005551111',
activeAt: Date.now(),
name: 'Á',
timestamp: 20,
@ -90,6 +97,7 @@ describe('state/selectors/conversations', () => {
type: 'direct',
isMe: false,
lastUpdated: Date.now(),
title: 'A',
unreadCount: 1,
isSelected: false,
typingContact: {
@ -102,6 +110,7 @@ describe('state/selectors/conversations', () => {
},
id5: {
id: 'id5',
e164: '+18005551111',
activeAt: Date.now(),
name: 'First!',
timestamp: 30,
@ -112,6 +121,7 @@ describe('state/selectors/conversations', () => {
type: 'direct',
isMe: false,
lastUpdated: Date.now(),
title: 'First!',
unreadCount: 1,
isSelected: false,
typingContact: {

17
ts/textsecure.d.ts vendored
View file

@ -96,6 +96,14 @@ type StoredSignedPreKeyType = SignedPreKeyType & {
created_at: number;
};
type IdentityKeyRecord = {
publicKey: ArrayBuffer;
firstUse: boolean;
timestamp: number;
verified: number;
nonblockingApproval: boolean;
};
export type StorageProtocolType = StorageType & {
VerifiedStatus: {
DEFAULT: number;
@ -105,6 +113,7 @@ export type StorageProtocolType = StorageType & {
archiveSiblingSessions: (identifier: string) => Promise<void>;
removeSession: (identifier: string) => Promise<void>;
getDeviceIds: (identifier: string) => Promise<Array<number>>;
getIdentityRecord: (identifier: string) => IdentityKeyRecord | undefined;
hydrateCaches: () => Promise<void>;
clearPreKeyStore: () => Promise<void>;
clearSignedPreKeysStore: () => Promise<void>;
@ -119,13 +128,7 @@ export type StorageProtocolType = StorageType & {
loadSignedPreKeys: () => Promise<Array<StoredSignedPreKeyType>>;
saveIdentityWithAttributes: (
number: string,
options: {
publicKey: ArrayBuffer;
firstUse: boolean;
timestamp: number;
verified: number;
nonblockingApproval: boolean;
}
options: IdentityKeyRecord
) => Promise<void>;
removeSignedPreKey: (keyId: number) => Promise<void>;
removeAllData: () => Promise<void>;

View file

@ -1,4 +1,4 @@
// tslint:disable no-backbone-get-set-outside-model no-default-export no-unnecessary-local-variable
// tslint:disable no-default-export no-unnecessary-local-variable
import EventTarget from './EventTarget';
import { WebAPIType } from './WebAPI';

View file

@ -1,4 +1,4 @@
// tslint:disable no-backbone-get-set-outside-model no-bitwise no-default-export
// tslint:disable no-bitwise no-default-export
import { without } from 'lodash';
import PQueue from 'p-queue';

View file

@ -189,7 +189,6 @@ const agents: AgentCacheType = {};
function getContentType(response: Response) {
if (response.headers && response.headers.get) {
// tslint:disable-next-line no-backbone-get-set-outside-model
return response.headers.get('content-type');
}
@ -310,7 +309,6 @@ async function _promiseAjax(
let resultPromise;
if (
options.responseType === 'json' &&
// tslint:disable-next-line no-backbone-get-set-outside-model
response.headers.get('Content-Type') === 'application/json'
) {
resultPromise = response.json();
@ -1464,7 +1462,6 @@ export function initialize({
throw new Error('makeProxiedRequest: Problem retrieving header value');
}
// tslint:disable-next-line no-backbone-get-set-outside-model
const range = response.headers.get('content-range');
const match = PARSE_RANGE_HEADER.exec(range);

View file

@ -6,6 +6,10 @@ import { createBatcher } from './batcher';
import { createWaitBatcher } from './waitBatcher';
import { deleteForEveryone } from './deleteForEveryone';
import { downloadAttachment } from './downloadAttachment';
import {
generateSecurityNumber,
getPlaceholder as getSafetyNumberPlaceholder,
} from './safetyNumber';
import { hasExpired } from './hasExpired';
import { isFileDangerous } from './isFileDangerous';
import { makeLookup } from './makeLookup';
@ -20,6 +24,8 @@ export {
createWaitBatcher,
deleteForEveryone,
downloadAttachment,
generateSecurityNumber,
getSafetyNumberPlaceholder,
GoogleChrome,
hasExpired,
isFileDangerous,

View file

@ -223,7 +223,7 @@
"rule": "jQuery-wrap(",
"path": "js/models/conversations.js",
"line": " await wrap(",
"lineNumber": 641,
"lineNumber": 644,
"reasonCategory": "falseMatch",
"updated": "2020-06-09T20:26:46.515Z"
},
@ -710,46 +710,18 @@
{
"rule": "jQuery-$(",
"path": "js/views/key_verification_view.js",
"line": " new QRCode(this.$('.qr')[0]).makeCode(",
"lineNumber": 43,
"line": " this.$('.key-verification-wrapper').append(view.el);",
"lineNumber": 23,
"reasonCategory": "usageTrusted",
"updated": "2020-02-14T20:02:37.507Z",
"reasonDetail": "Hardcoded selector"
"updated": "2020-06-23T06:48:06.829Z"
},
{
"rule": "jQuery-wrap(",
"rule": "jQuery-append(",
"path": "js/views/key_verification_view.js",
"line": " dcodeIO.ByteBuffer.wrap(this.ourKey).toString('base64')",
"lineNumber": 44,
"reasonCategory": "falseMatch",
"updated": "2020-02-14T20:02:37.507Z"
},
{
"rule": "jQuery-insertBefore(",
"path": "js/views/key_verification_view.js",
"line": " dialog.$el.insertBefore(this.el);",
"lineNumber": 86,
"line": " this.$('.key-verification-wrapper').append(view.el);",
"lineNumber": 23,
"reasonCategory": "usageTrusted",
"updated": "2020-02-14T20:02:37.507Z",
"reasonDetail": "Known DOM elements"
},
{
"rule": "jQuery-$(",
"path": "js/views/key_verification_view.js",
"line": " this.$('button.verify').attr('disabled', true);",
"lineNumber": 90,
"reasonCategory": "usageTrusted",
"updated": "2020-02-14T20:02:37.507Z",
"reasonDetail": "Hardcoded selector"
},
{
"rule": "jQuery-$(",
"path": "js/views/key_verification_view.js",
"line": " this.$('button.verify').removeAttr('disabled');",
"lineNumber": 121,
"reasonCategory": "usageTrusted",
"updated": "2020-02-14T20:02:37.507Z",
"reasonDetail": "Hardcoded selector"
"updated": "2020-06-23T06:48:06.829Z"
},
{
"rule": "jQuery-append(",
@ -841,6 +813,22 @@
"updated": "2018-10-11T19:22:47.331Z",
"reasonDetail": "Operating on already-existing DOM elements"
},
{
"rule": "jQuery-$(",
"path": "js/views/safety_number_change_dialog_view.js",
"line": " this.$('.safety-number-change-dialog-wrapper').append(dialog.el);",
"lineNumber": 36,
"reasonCategory": "usageTrusted",
"updated": "2020-06-23T06:48:06.829Z"
},
{
"rule": "jQuery-append(",
"path": "js/views/safety_number_change_dialog_view.js",
"line": " this.$('.safety-number-change-dialog-wrapper').append(dialog.el);",
"lineNumber": 36,
"reasonCategory": "usageTrusted",
"updated": "2020-06-23T06:48:06.829Z"
},
{
"rule": "jQuery-$(",
"path": "js/views/settings_view.js",
@ -11579,6 +11567,15 @@
"updated": "2020-02-14T20:02:37.507Z",
"reasonDetail": "Used only to set focus"
},
{
"rule": "React-createRef",
"path": "ts/components/SafetyNumberChangeDialog.js",
"line": " const cancelButtonRef = React.createRef();",
"lineNumber": 14,
"reasonCategory": "usageTrusted",
"updated": "2020-06-23T06:48:06.829Z",
"reasonDetail": "Used to focus cancel button when dialog opens"
},
{
"rule": "React-createRef",
"path": "ts/components/SearchResults.js",

View file

@ -13,11 +13,9 @@ export function remove() {
}
export function isDone() {
// tslint:disable-next-line no-backbone-get-set-outside-model
return window.storage.get('chromiumRegistrationDone') === '';
}
export function everDone() {
// tslint:disable-next-line no-backbone-get-set-outside-model
return window.storage.get('chromiumRegistrationDoneEver') === '' || isDone();
}

58
ts/util/safetyNumber.ts Normal file
View file

@ -0,0 +1,58 @@
import { ConversationType } from '../state/ducks/conversations';
export async function generateSecurityNumber(
ourNumber: string,
ourKey: ArrayBuffer,
theirNumber: string,
theirKey: ArrayBuffer
): Promise<string> {
return new window.libsignal.FingerprintGenerator(5200).createFor(
ourNumber,
ourKey,
theirNumber,
theirKey
);
}
export function getPlaceholder(): string {
return Array.from(Array(12))
.map(() => 'XXXXX')
.join(' ');
}
export async function generateSecurityNumberBlock(
contact: ConversationType
): Promise<Array<string>> {
const ourNumber = window.textsecure.storage.user.getNumber();
const ourUuid = window.textsecure.storage.user.getUuid();
const us = window.textsecure.storage.protocol.getIdentityRecord(
ourUuid || ourNumber
);
const ourKey = us ? us.publicKey : null;
const them = window.textsecure.storage.protocol.getIdentityRecord(contact.id);
const theirKey = them ? them.publicKey : null;
if (!ourKey) {
throw new Error('Could not load our key');
}
if (!theirKey) {
throw new Error('Could not load their key');
}
const securityNumber = await generateSecurityNumber(
ourNumber,
ourKey,
contact.e164,
theirKey
);
const chunks = [];
for (let i = 0; i < securityNumber.length; i += 5) {
chunks.push(securityNumber.substring(i, i + 5));
}
return chunks;
}

7
ts/window.d.ts vendored
View file

@ -13,9 +13,12 @@ import * as Crypto from './Crypto';
import { ColorType, LocalizerType } from './types/Util';
import { SendOptionsType } from './textsecure/SendMessage';
type TaskResultType = any;
declare global {
interface Window {
dcodeIO: DCodeIOType;
getConversations: () => ConversationControllerType;
getExpiration: () => string;
getEnvironment: () => string;
getSocketStatus: () => number;
@ -83,12 +86,16 @@ export type ConversationType = {
getColor(): ColorType | undefined;
getName(): string | undefined;
getNumber(): string;
getProfiles(): Promise<Array<Promise<void>>>;
getProfileName(): string | undefined;
getRecipients: () => Array<string>;
getSendOptions(): SendOptionsType;
getTitle(): string;
isVerified(): boolean;
safeGetVerified(): Promise<number>;
getIsAddedByContact(): boolean;
addCallHistory(details: CallHistoryDetailsType): void;
toggleVerified(): Promise<TaskResultType>;
};
export type ConversationControllerType = {

View file

@ -45,6 +45,9 @@
// We always want 'as Type'
"no-angle-bracket-type-assertion": true,
// mostly always a false positive
"no-backbone-get-set-outside-model": false,
"no-consecutive-blank-lines": [true, 2],
"object-literal-key-quotes": [true, "as-needed"],
"object-literal-sort-keys": false,