Add support for ACI safety numbers behind a feature flag

This commit is contained in:
Fedor Indutny 2023-07-13 21:06:42 +02:00 committed by Fedor Indutnyy
parent 42cd8ce792
commit c1580a5eb3
38 changed files with 1392 additions and 204 deletions

View file

@ -1655,6 +1655,55 @@ Signal Desktop makes use of the following open source projects.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
## lottie-react
The MIT License
Copyright David Gamote and other contributors.
This software consists of voluntary contributions made by many
individuals. For exact contribution history, see the revision history
available on GitHub.
The following license applies to all parts of this software except as
documented below:
====
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
====
Copyright and related rights for sample code are waived via CC0. Sample
code is defined as all source code displayed within the prose of the
documentation.
CC0: http://creativecommons.org/publicdomain/zero/1.0/
====
Files located in the node_modules and vendor directories are externally
maintained libraries used by this software which have their own
licenses; we recommend you read them, as their terms may differ from the
terms above.
## lru-cache ## lru-cache
The ISC License The ISC License

View file

@ -634,6 +634,26 @@
"messageformat": "Accept", "messageformat": "Accept",
"description": "Label for a button to accept a new safety number" "description": "Label for a button to accept a new safety number"
}, },
"icu:SafetyNumberViewer__migration__text": {
"messageformat": "Safety numbers are being updated.",
"description": "An explanatory note in SafetyNumberViewer describing the safety number migration process."
},
"icu:SafetyNumberViewer__migration__learn_more": {
"messageformat": "Learn more",
"description": "A link text in SafetyNumberViewer describing the safety number migration process."
},
"icu:SafetyNumberViewer__card__prev": {
"messageformat": "Previous Safety number",
"description": "An ARIA label for safety number navigation button."
},
"icu:SafetyNumberViewer__card__next": {
"messageformat": "Next Safety number",
"description": "An ARIA label for safety number navigation button."
},
"icu:SafetyNumberViewer__carousel__dot": {
"messageformat": "Safety number version, {index, number} of {total, number}",
"description": "An ARIA label for safety number carousel button."
},
"icu:SafetyNumberViewer__markAsVerified": { "icu:SafetyNumberViewer__markAsVerified": {
"messageformat": "Mark as verified", "messageformat": "Mark as verified",
"description": "Safety number viewer, verification toggle button, when not verified, sets verified" "description": "Safety number viewer, verification toggle button, when not verified, sets verified"
@ -642,6 +662,42 @@
"messageformat": "Clear verification", "messageformat": "Clear verification",
"description": "Safety number viewer, verification toggle button, when verified, clears verification state" "description": "Safety number viewer, verification toggle button, when verified, clears verification state"
}, },
"icu:SafetyNumberViewer__hint--migration": {
"messageformat": "To verify end-to-end encryption with {name}, match the color card above with their device and compare the numbers. If these dont match, try the other pair of safety numbers. Only one pair needs to match.",
"description": "Safety number viewer, text of the hint during migration period"
},
"icu:SafetyNumberViewer__hint--normal": {
"messageformat": "To verify end-to-end encryption with {name}, compare the numbers above with their device. They can also scan your code with their device.",
"description": "Safety number viewer, text of the hint after migration period"
},
"icu:SafetyNumberOnboarding__title": {
"messageformat": "Changes to safety numbers",
"description": "Title of Safety number onboarding modal"
},
"icu:SafetyNumberOnboarding__p1": {
"messageformat": "Safety numbers are being updated over a transition period to enable upcoming privacy features in Signal.",
"description": "Paragraph 1 of Safety number onboarding modal"
},
"icu:SafetyNumberOnboarding__p2": {
"messageformat": "To verify safety numbers, match the color card with your contacts device. If these dont match, try the other pair of safety numbers. Only one pair needs to match.",
"description": "Paragraph 2 of Safety number onboarding modal"
},
"icu:SafetyNumberOnboarding__help": {
"messageformat": "Need help?",
"description": "Text of a secondary button in Safety number onboarding modal"
},
"icu:SafetyNumberOnboarding__close": {
"messageformat": "Got it",
"description": "Text of a secondary button in Safety number onboarding modal"
},
"icu:SafetyNumberNotReady__body": {
"messageformat": "A safety number will be created with this person after you exchange messages with them.",
"description": "Body of SafetyNumberNotReady modal"
},
"icu:SafetyNumberNotReady__learn-more": {
"messageformat": "Learn more",
"description": "Text of 'Learn more' button of SafetyNumberNotReady modal"
},
"icu:isVerified": { "icu:isVerified": {
"messageformat": "You have verified your safety number with {name}.", "messageformat": "You have verified your safety number with {name}.",
"description": "Summary state shown at top of the safety number screen if user has verified contact." "description": "Summary state shown at top of the safety number screen if user has verified contact."
@ -1211,7 +1267,8 @@
"description": "This is a menu item for viewing all media (images + video) in a conversation, using the imperative case, as in a command." "description": "This is a menu item for viewing all media (images + video) in a conversation, using the imperative case, as in a command."
}, },
"icu:verifyHelp": { "icu:verifyHelp": {
"messageformat": "To verify the security of your end-to-end encryption with {name}, compare the numbers above with their device." "messageformat": "To verify the security of your end-to-end encryption with {name}, compare the numbers above with their device. They can also scan the qr code above.",
"description": "(deleted 07/05/2023)"
}, },
"icu:theirIdentityUnknown": { "icu:theirIdentityUnknown": {
"messageformat": "You haven't exchanged any messages with this contact yet. Your safety number with them will be available after the first message." "messageformat": "You haven't exchanged any messages with this contact yet. Your safety number with them will be available after the first message."

View file

@ -0,0 +1 @@
<svg width="10" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.82 3.534a.73.73 0 0 0-.995.27L2.492 9.639a.73.73 0 0 0 0 .724l3.333 5.833a.73.73 0 1 0 1.267-.723L3.965 10l3.127-5.472a.73.73 0 0 0-.272-.994Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 250 B

View file

@ -0,0 +1 @@
<svg width="10" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.18 3.534a.73.73 0 0 1 .995.27l3.333 5.834a.73.73 0 0 1 0 .724l-3.333 5.833a.73.73 0 1 1-1.266-.723L6.035 10 2.91 4.528a.73.73 0 0 1 .271-.994Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 250 B

View file

@ -0,0 +1,42 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="29.3486" y="6" width="20" height="32" rx="4" transform="rotate(25 29.3486 6)" fill="#86A1E3" stroke="#3D62BC" stroke-width="1.75"/>
<g clip-path="url(#clip0_8_3611)">
<path d="M36.7111 19.4259C36.9746 19.2614 37.0549 18.9144 36.8904 18.6509C36.7259 18.3873 36.3789 18.3071 36.1153 18.4716L32.9916 20.4217L32.6913 19.1078C32.622 18.805 32.3204 18.6156 32.0176 18.6848C31.7147 18.754 31.5253 19.0557 31.5946 19.3585L32.0709 21.4426C32.1115 21.62 32.2353 21.7668 32.4033 21.8367C32.5714 21.9065 32.7628 21.8908 32.9172 21.7944L36.7111 19.4259Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.6389 24.3772C30.9879 25.0957 31.7929 25.4711 32.5677 25.2766L34.8402 24.706C36.1636 24.3738 37.2969 23.5214 37.9831 22.3421L39.3586 19.9783C39.7524 19.3014 39.6289 18.4426 39.0602 17.9042L36.9889 15.943C36.4292 15.4131 35.7127 15.0789 34.9469 14.9908L32.1132 14.6647C31.3353 14.5752 30.5979 15.0326 30.3326 15.7694L29.4059 18.3425C28.9436 19.6262 29.0191 21.0422 29.6152 22.2696L30.6389 24.3772ZM32.2938 24.1855C32.0355 24.2503 31.7672 24.1252 31.6508 23.8857L30.6272 21.7781C30.1635 20.8235 30.1048 19.7221 30.4644 18.7236L31.3911 16.1505C31.4795 15.905 31.7253 15.7525 31.9846 15.7823L34.8183 16.1084C35.3422 16.1687 35.8325 16.3973 36.2155 16.76L38.2868 18.7211C38.4763 18.9005 38.5175 19.1868 38.3862 19.4124L37.0107 21.7763C36.477 22.6935 35.5956 23.3565 34.5663 23.6149L32.2938 24.1855Z" fill="white"/>
<path d="M36.7111 19.4259C36.9746 19.2614 37.0549 18.9144 36.8904 18.6509C36.7259 18.3873 36.3789 18.3071 36.1153 18.4716L32.9916 20.4217L32.6913 19.1078C32.622 18.805 32.3204 18.6156 32.0176 18.6848C31.7147 18.754 31.5253 19.0557 31.5946 19.3585L32.0709 21.4426C32.1115 21.62 32.2353 21.7668 32.4033 21.8367C32.5714 21.9065 32.7628 21.8908 32.9172 21.7944L36.7111 19.4259Z" stroke="white" stroke-width="0.25"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.6389 24.3772C30.9879 25.0957 31.7929 25.4711 32.5677 25.2766L34.8402 24.706C36.1636 24.3738 37.2969 23.5214 37.9831 22.3421L39.3586 19.9783C39.7524 19.3014 39.6289 18.4426 39.0602 17.9042L36.9889 15.943C36.4292 15.4131 35.7127 15.0789 34.9469 14.9908L32.1132 14.6647C31.3353 14.5752 30.5979 15.0326 30.3326 15.7694L29.4059 18.3425C28.9436 19.6262 29.0191 21.0422 29.6152 22.2696L30.6389 24.3772ZM32.2938 24.1855C32.0355 24.2503 31.7672 24.1252 31.6508 23.8857L30.6272 21.7781C30.1635 20.8235 30.1048 19.7221 30.4644 18.7236L31.3911 16.1505C31.4795 15.905 31.7253 15.7525 31.9846 15.7823L34.8183 16.1084C35.3422 16.1687 35.8325 16.3973 36.2155 16.76L38.2868 18.7211C38.4763 18.9005 38.5175 19.1868 38.3862 19.4124L37.0107 21.7763C36.477 22.6935 35.5956 23.3565 34.5663 23.6149L32.2938 24.1855Z" stroke="white" stroke-width="0.25"/>
</g>
<path d="M28.6304 28.8359L30.443 29.6812" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<path d="M24.0991 26.7229L25.9117 27.5681" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<path d="M33.1621 30.9492L34.9747 31.7945" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<path d="M27.3628 31.5549L29.1754 32.4002" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<path d="M22.8311 29.4419L24.6437 30.2871" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<path d="M31.894 33.668L33.7067 34.5132" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<path d="M26.0947 34.2739L27.9073 35.1192" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<path d="M21.563 32.1609L23.3756 33.0061" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<path d="M30.6265 36.387L32.4391 37.2322" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<rect y="14.4524" width="20" height="32" rx="4" transform="rotate(-25 0 14.4524)" fill="#EBEAE8" stroke="#837D72" stroke-width="1.75"/>
<g clip-path="url(#clip1_8_3611)">
<path d="M15.0172 17.4425C15.0605 17.1348 14.8463 16.8503 14.5387 16.807C14.2311 16.7636 13.9465 16.9778 13.9032 17.2854L13.3892 20.9319L12.1896 20.3174C11.9131 20.1758 11.5742 20.2851 11.4325 20.5616C11.2909 20.8381 11.4002 21.177 11.6767 21.3187L13.5794 22.2934C13.7414 22.3763 13.9334 22.3758 14.095 22.292C14.2565 22.2082 14.3675 22.0514 14.3929 21.8712L15.0172 17.4425Z" fill="#837D72"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.9069 25.2767C15.6817 25.4712 16.4868 25.0958 16.8358 24.3772L17.8594 22.2696C18.4556 21.0423 18.531 19.6263 18.0687 18.3425L17.1421 15.7694C16.8767 15.0327 16.1394 14.5752 15.3615 14.6648L12.5277 14.9909C11.762 15.079 11.0455 15.4131 10.4857 15.9431L8.41443 17.9042C7.84579 18.4426 7.72225 19.3015 8.11609 19.9783L9.49156 22.3421C10.1778 23.5215 11.311 24.3739 12.6344 24.7061L14.9069 25.2767ZM15.8238 23.8857C15.7075 24.1252 15.4391 24.2504 15.1809 24.1855L12.9084 23.615C11.8791 23.3566 10.9977 22.6936 10.4639 21.7763L9.08846 19.4125C8.95717 19.1869 8.99835 18.9006 9.1879 18.7211L11.2592 16.76C11.6422 16.3974 12.1324 16.1688 12.6564 16.1085L15.4901 15.7824C15.7494 15.7525 15.9952 15.905 16.0836 16.1506L17.0103 18.7237C17.3698 19.7222 17.3111 20.8235 16.8475 21.7781L15.8238 23.8857Z" fill="#837D72"/>
<path d="M15.0172 17.4425C15.0605 17.1348 14.8463 16.8503 14.5387 16.807C14.2311 16.7636 13.9465 16.9778 13.9032 17.2854L13.3892 20.9319L12.1896 20.3174C11.9131 20.1758 11.5742 20.2851 11.4325 20.5616C11.2909 20.8381 11.4002 21.177 11.6767 21.3187L13.5794 22.2934C13.7414 22.3763 13.9334 22.3758 14.095 22.292C14.2565 22.2082 14.3675 22.0514 14.3929 21.8712L15.0172 17.4425Z" stroke="#837D72" stroke-width="0.25"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.9069 25.2767C15.6817 25.4712 16.4868 25.0958 16.8358 24.3772L17.8594 22.2696C18.4556 21.0423 18.531 19.6263 18.0687 18.3425L17.1421 15.7694C16.8767 15.0327 16.1394 14.5752 15.3615 14.6648L12.5277 14.9909C11.762 15.079 11.0455 15.4131 10.4857 15.9431L8.41443 17.9042C7.84579 18.4426 7.72225 19.3015 8.11609 19.9783L9.49156 22.3421C10.1778 23.5215 11.311 24.3739 12.6344 24.7061L14.9069 25.2767ZM15.8238 23.8857C15.7075 24.1252 15.4391 24.2504 15.1809 24.1855L12.9084 23.615C11.8791 23.3566 10.9977 22.6936 10.4639 21.7763L9.08846 19.4125C8.95717 19.1869 8.99835 18.9006 9.1879 18.7211L11.2592 16.76C11.6422 16.3974 12.1324 16.1688 12.6564 16.1085L15.4901 15.7824C15.7494 15.7525 15.9952 15.905 16.0836 16.1506L17.0103 18.7237C17.3698 19.7222 17.3111 20.8235 16.8475 21.7781L15.8238 23.8857Z" stroke="#837D72" stroke-width="0.25"/>
</g>
<path d="M17.0317 29.6814L18.8444 28.8362" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
<path d="M12.5 31.7944L14.3126 30.9492" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
<path d="M21.5635 27.5681L23.3761 26.7229" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
<path d="M18.2998 32.4001L20.1124 31.5549" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
<path d="M13.7681 34.5134L15.5807 33.6682" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
<path d="M22.8311 30.2871L24.6437 29.4419" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
<path d="M19.5674 35.1191L21.38 34.2739" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
<path d="M15.0361 37.2322L16.8487 36.3869" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
<path d="M24.0991 33.0061L25.9117 32.1609" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
<defs>
<clipPath id="clip0_8_3611">
<rect width="12" height="12" fill="white" transform="translate(30.8608 12.2219) rotate(25)"/>
</clipPath>
<clipPath id="clip1_8_3611">
<rect width="12" height="12" fill="white" transform="translate(5.73828 17.2935) rotate(-25)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.5 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -127,6 +127,7 @@
"linkify-it": "2.2.0", "linkify-it": "2.2.0",
"lodash": "4.17.21", "lodash": "4.17.21",
"long": "4.0.0", "long": "4.0.0",
"lottie-react": "2.4.0",
"lru-cache": "6.0.0", "lru-cache": "6.0.0",
"mac-screen-capture-permissions": "2.0.0", "mac-screen-capture-permissions": "2.0.0",
"memoizee": "0.4.14", "memoizee": "0.4.14",

View file

@ -0,0 +1,26 @@
diff --git a/node_modules/qrcode-generator/qrcode.d.ts b/node_modules/qrcode-generator/qrcode.d.ts
index b34952e..b394c44 100644
--- a/node_modules/qrcode-generator/qrcode.d.ts
+++ b/node_modules/qrcode-generator/qrcode.d.ts
@@ -36,7 +36,7 @@ interface QRCodeFactory {
}
interface QRCode {
- addData(data: string, mode?: Mode) : void;
+ addData(data: string | Uint8Array, mode?: Mode) : void;
make() : void;
getModuleCount() : number;
isDark(row: number, col: number) : boolean;
diff --git a/node_modules/qrcode-generator/qrcode.js b/node_modules/qrcode-generator/qrcode.js
index 76889b5..f4d8c88 100644
--- a/node_modules/qrcode-generator/qrcode.js
+++ b/node_modules/qrcode-generator/qrcode.js
@@ -1728,7 +1728,7 @@ var qrcode = function() {
var _mode = QRMode.MODE_8BIT_BYTE;
var _data = data;
- var _bytes = qrcode.stringToBytes(data);
+ var _bytes = data instanceof Uint8Array ? data : qrcode.stringToBytes(data);
var _this = {};

View file

@ -226,6 +226,8 @@ $rtl-icon-map: (
// v3 icons // v3 icons
'chevron-left.svg': 'chevron-right.svg', 'chevron-left.svg': 'chevron-right.svg',
'chevron-right.svg': 'chevron-left.svg', 'chevron-right.svg': 'chevron-left.svg',
'chevron-shallow-left.svg': 'chevron-shallow-right.svg',
'chevron-shallow-right.svg': 'chevron-shallow-left.svg',
'chevron-left-compact-bold.svg': 'chevron-right-compact-bold.svg', 'chevron-left-compact-bold.svg': 'chevron-right-compact-bold.svg',
'chevron-right-compact-bold.svg': 'chevron-left-compact-bold.svg', 'chevron-right-compact-bold.svg': 'chevron-left-compact-bold.svg',
'arrow-left.svg': 'arrow-right.svg', 'arrow-left.svg': 'arrow-right.svg',

View file

@ -80,6 +80,8 @@ $color-violet: #9932c8;
$color-plum: #aa377a; $color-plum: #aa377a;
$color-taupe: #8f616a; $color-taupe: #8f616a;
$color-steel: #71717f; $color-steel: #71717f;
$color-bright-gray: #ebeae8;
$color-borage-blue: #506ecd;
// Gradient colors // Gradient colors

View file

@ -0,0 +1,52 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.module-SafetyNumberOnboarding {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
h2 {
margin-block: 0 12px;
}
p {
margin-block: 0 25px;
text-align: start;
@include font-body-2;
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}
svg {
margin-block: 0 21px;
border-radius: 12px;
@include light-theme {
border: 1.5px solid $color-black-alpha-12;
}
@include dark-theme {
border: 1.5px solid $color-gray-60;
}
background: $color-white;
}
&__help {
a {
@include font-body-1-bold;
text-decoration: none;
}
margin-block: 0 24px;
}
&__close {
width: 296px;
margin-bottom: 16px;
}
}

View file

@ -2,67 +2,206 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
.module-SafetyNumberViewer { .module-SafetyNumberViewer {
display: flex;
flex-direction: column;
align-items: center;
text-align: center; text-align: center;
gap: 16px;
a {
text-decoration: none;
}
&__migration {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
width: 100%;
margin-block: 0 8px;
&__text {
flex-grow: 1;
text-align: start;
@include font-subtitle;
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
p {
margin: 0;
}
}
&__icon { &__icon {
height: 1.25em; flex-shrink: 0;
width: 1.25em;
vertical-align: text-bottom;
display: inline-block; display: inline-block;
} width: 48px;
height: 48px;
&__icon--verified { background: url('../images/safety-number-migration.svg');
display: inline-block;
height: 20px;
margin-inline-end: 4px;
vertical-align: text-bottom;
width: 20px;
@include light-theme {
@include color-svg('../images/icons/v3/check/check.svg', $color-gray-95);
}
@include dark-theme {
@include color-svg('../images/icons/v3/check/check.svg', $color-gray-02);
} }
} }
&__icon--shield { &__card-container {
display: inline-block; position: relative;
height: 20px; display: flex;
margin-inline-end: 4px; align-items: center;
vertical-align: text-bottom; justify-content: center;
width: 20px; width: 100%;
@include light-theme {
@include color-svg(
'../images/icons/v3/safety_number/safety_number.svg',
$color-gray-95
);
} }
@include dark-theme { &__card {
@include color-svg( display: flex;
'../images/icons/v3/safety_number/safety_number.svg', flex-direction: column;
$color-gray-02 gap: 16px;
); align-items: center;
max-width: 248px;
padding: 24px;
border-radius: 12px;
&--e164 {
background-color: $color-bright-gray;
.module-SafetyNumberViewer__card__number {
color: $color-gray-75;
} }
} }
&--aci {
background-color: $color-borage-blue;
.module-SafetyNumberViewer__card__number {
color: $color-white;
}
}
&__qr {
width: 120px;
height: 120px;
padding: 10px;
border-radius: 8px;
background: $color-white;
}
&__number { &__number {
border-radius: 8px; display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
font-family: $monospace; font-family: $monospace;
height: 100px; margin-block: 0 4px;
margin-block: 24px;
margin-inline: auto; @include keyboard-mode {
padding: 24px; &:focus {
width: 245px; box-shadow: 0 0 0 3px $color-ultramarine;
}
}
}
&__prev,
&__next {
--height: 36px;
position: absolute;
top: calc(50% - var(--height) / 2);
width: 18px;
height: var(--height);
opacity: 0;
transition: opacity 200ms ease-in-out;
&:hover {
opacity: 1;
}
}
&__prev {
inset-inline-start: 0;
@include light-theme {
@include color-svg(
'../images/icons/v3/chevron/chevron-shallow-left.svg',
$color-gray-75
);
}
@include dark-theme {
@include color-svg(
'../images/icons/v3/chevron/chevron-shallow-left.svg',
$color-gray-15
);
}
}
&__next {
inset-inline-end: 0;
@include light-theme {
@include color-svg(
'../images/icons/v3/chevron/chevron-shallow-right.svg',
$color-gray-75
);
}
@include dark-theme {
@include color-svg(
'../images/icons/v3/chevron/chevron-shallow-right.svg',
$color-gray-15
);
}
}
}
&__carousel {
display: flex;
gap: 8px;
&__dot {
@include button-reset;
width: 8px;
height: 8px;
border-radius: 4px;
@include light-theme { @include light-theme {
background-color: $color-gray-02; background: $color-gray-25;
} }
@include dark-theme { @include dark-theme {
background-color: $color-gray-90; background: $color-gray-60;
}
@include keyboard-mode {
&:focus {
box-shadow: 0 0 0 3px $color-ultramarine;
}
}
&[aria-pressed='true'] {
@include light-theme {
background: $color-black;
}
@include dark-theme {
background: $color-white;
}
}
}
}
&__help {
@include font-subtitle;
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
} }
} }
@ -72,12 +211,12 @@
text-align: center; text-align: center;
} }
&__buttons { &__button {
text-align: end; margin-block: 0 16px;
} }
&__button { &__buttons {
margin-top: 30px; text-align: end;
} }
&__modal.module-Modal { &__modal.module-Modal {

View file

@ -116,6 +116,7 @@
@import './components/ReactionPickerPicker.scss'; @import './components/ReactionPickerPicker.scss';
@import './components/RecordingComposer.scss'; @import './components/RecordingComposer.scss';
@import './components/SafetyNumberChangeDialog.scss'; @import './components/SafetyNumberChangeDialog.scss';
@import './components/SafetyNumberOnboarding.scss';
@import './components/SafetyNumberViewer.scss'; @import './components/SafetyNumberViewer.scss';
@import './components/ScrollDownButton.scss'; @import './components/ScrollDownButton.scss';
@import './components/SearchInput.scss'; @import './components/SearchInput.scss';

View file

@ -7,6 +7,7 @@ import type { WebAPIType } from './textsecure/WebAPI';
import * as log from './logging/log'; import * as log from './logging/log';
import type { UUIDStringType } from './types/UUID'; import type { UUIDStringType } from './types/UUID';
import { parseIntOrThrow } from './util/parseIntOrThrow'; import { parseIntOrThrow } from './util/parseIntOrThrow';
import { SECOND, HOUR } from './util/durations';
import * as Bytes from './Bytes'; import * as Bytes from './Bytes';
import { hash, uuidToBytes } from './Crypto'; import { hash, uuidToBytes } from './Crypto';
import { HashType } from './types/Crypto'; import { HashType } from './types/Crypto';
@ -29,8 +30,7 @@ export type ConfigKeyType =
| 'desktop.messageRequests' | 'desktop.messageRequests'
| 'desktop.pnp' | 'desktop.pnp'
| 'desktop.retryRespondMaxAge' | 'desktop.retryRespondMaxAge'
| 'desktop.safetyNumberUUID.timestamp' | 'desktop.safetyNumberAci'
| 'desktop.safetyNumberUUID'
| 'desktop.senderKey.retry' | 'desktop.senderKey.retry'
| 'desktop.senderKey.send' | 'desktop.senderKey.send'
| 'desktop.senderKeyMaxAge' | 'desktop.senderKeyMaxAge'
@ -47,7 +47,8 @@ export type ConfigKeyType =
| 'global.groupsv2.groupSizeHardLimit' | 'global.groupsv2.groupSizeHardLimit'
| 'global.groupsv2.maxGroupSize' | 'global.groupsv2.maxGroupSize'
| 'global.nicknames.max' | 'global.nicknames.max'
| 'global.nicknames.min'; | 'global.nicknames.min'
| 'global.safetyNumberAci';
type ConfigValueType = { type ConfigValueType = {
name: ConfigKeyType; name: ConfigKeyType;
@ -88,7 +89,15 @@ export const refreshRemoteConfig = async (
server: WebAPIType server: WebAPIType
): Promise<void> => { ): Promise<void> => {
const now = Date.now(); const now = Date.now();
const newConfig = await server.getConfig(); const { config: newConfig, serverEpochTime } = await server.getConfig();
const serverTimeSkew = serverEpochTime * SECOND - now;
if (Math.abs(serverTimeSkew) > HOUR) {
log.warn(
'Remote Config: sever clock skew detected. ' +
`Server time ${serverEpochTime * SECOND}, local time ${now}`
);
}
// Process new configuration in light of the old configuration // Process new configuration in light of the old configuration
// The old configuration is not set as the initial value in reduce because // The old configuration is not set as the initial value in reduce because
@ -129,6 +138,7 @@ export const refreshRemoteConfig = async (
}, {}); }, {});
await window.storage.put('remoteConfig', config); await window.storage.put('remoteConfig', config);
await window.storage.put('serverTimeSkew', serverTimeSkew);
}; };
export const maybeRefreshRemoteConfig = throttle( export const maybeRefreshRemoteConfig = throttle(

View file

@ -12,7 +12,7 @@ const ERROR_CORRECTION_LEVEL = 'L';
type PropsType = Readonly<{ type PropsType = Readonly<{
alt: string; alt: string;
className?: string; className?: string;
data: string; data: string | Uint8Array;
}>; }>;
export function QrCode(props: PropsType): ReactElement { export function QrCode(props: PropsType): ReactElement {
@ -37,6 +37,9 @@ export function QrCode(props: PropsType): ReactElement {
if (getEnvironment() === Environment.Production) { if (getEnvironment() === Environment.Production) {
return; return;
} }
if (data instanceof Uint8Array) {
return;
}
void navigator.clipboard.writeText(data); void navigator.clipboard.writeText(data);

View file

@ -1,34 +1,81 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React, { useState, useCallback } from 'react';
import { SafetyNumberMode } from '../types/safetyNumber';
import { isSafetyNumberNotAvailable } from '../util/isSafetyNumberNotAvailable';
import { Modal } from './Modal'; import { Modal } from './Modal';
import type { PropsType as SafetyNumberViewerPropsType } from './SafetyNumberViewer'; import type { PropsType as SafetyNumberViewerPropsType } from './SafetyNumberViewer';
import { SafetyNumberViewer } from './SafetyNumberViewer'; import { SafetyNumberViewer } from './SafetyNumberViewer';
import { SafetyNumberOnboarding } from './SafetyNumberOnboarding';
import { SafetyNumberNotReady } from './SafetyNumberNotReady';
type PropsType = { type PropsType = {
toggleSafetyNumberModal: () => unknown; toggleSafetyNumberModal: () => unknown;
hasCompletedSafetyNumberOnboarding: boolean;
markHasCompletedSafetyNumberOnboarding: () => unknown;
} & Omit<SafetyNumberViewerPropsType, 'onClose'>; } & Omit<SafetyNumberViewerPropsType, 'onClose'>;
export function SafetyNumberModal({ export function SafetyNumberModal({
i18n, i18n,
toggleSafetyNumberModal, toggleSafetyNumberModal,
hasCompletedSafetyNumberOnboarding,
markHasCompletedSafetyNumberOnboarding,
...safetyNumberViewerProps ...safetyNumberViewerProps
}: PropsType): JSX.Element | null { }: PropsType): JSX.Element | null {
return ( const { contact, safetyNumberMode } = safetyNumberViewerProps;
<Modal
modalName="SafetyNumberModal" const [isOnboarding, setIsOnboarding] = useState(
hasXButton safetyNumberMode === SafetyNumberMode.ACIAndE164 &&
!hasCompletedSafetyNumberOnboarding
);
const showOnboarding = useCallback(() => {
setIsOnboarding(true);
}, [setIsOnboarding]);
const hideOnboarding = useCallback(() => {
setIsOnboarding(false);
markHasCompletedSafetyNumberOnboarding();
}, [setIsOnboarding, markHasCompletedSafetyNumberOnboarding]);
let title: string | undefined;
let content: JSX.Element;
let hasXButton = true;
if (isSafetyNumberNotAvailable(contact)) {
content = (
<SafetyNumberNotReady
i18n={i18n} i18n={i18n}
moduleClassName="module-SafetyNumberViewer__modal" onClose={() => toggleSafetyNumberModal()}
onClose={toggleSafetyNumberModal} />
title={i18n('icu:SafetyNumberModal__title')} );
> hasXButton = false;
} else if (isOnboarding) {
content = <SafetyNumberOnboarding i18n={i18n} onClose={hideOnboarding} />;
} else {
title = i18n('icu:SafetyNumberModal__title');
content = (
<SafetyNumberViewer <SafetyNumberViewer
i18n={i18n} i18n={i18n}
onClose={toggleSafetyNumberModal} onClose={toggleSafetyNumberModal}
showOnboarding={showOnboarding}
{...safetyNumberViewerProps} {...safetyNumberViewerProps}
/> />
);
}
return (
<Modal
modalName="SafetyNumberModal"
hasXButton={hasXButton}
i18n={i18n}
moduleClassName="module-SafetyNumberViewer__modal"
onClose={toggleSafetyNumberModal}
title={title}
>
{content}
</Modal> </Modal>
); );
} }

View file

@ -0,0 +1,23 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { action } from '@storybook/addon-actions';
import { SafetyNumberNotReady } from './SafetyNumberNotReady';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
export default {
title: 'Components/SafetyNumberNotReady',
};
export function Default(): JSX.Element {
return <SafetyNumberNotReady i18n={i18n} onClose={action('close')} />;
}
Default.story = {
name: 'Safety Number Not Ready',
};

View file

@ -0,0 +1,42 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { Button, ButtonVariant } from './Button';
import { Modal } from './Modal';
import { Intl } from './Intl';
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
import type { LocalizerType } from '../types/Util';
import { SAFETY_NUMBER_MIGRATION_URL } from '../types/support';
export type PropsType = {
i18n: LocalizerType;
onClose: () => void;
};
function onLearnMore() {
openLinkInWebBrowser(SAFETY_NUMBER_MIGRATION_URL);
}
export function SafetyNumberNotReady({
i18n,
onClose,
}: PropsType): JSX.Element | null {
return (
<div className="module-SafetyNumberNotReady">
<div>
<Intl i18n={i18n} id="icu:SafetyNumberNotReady__body" />
</div>
<Modal.ButtonFooter>
<Button onClick={onLearnMore} variant={ButtonVariant.Secondary}>
<Intl i18n={i18n} id="icu:SafetyNumberNotReady__learn-more" />
</Button>
<Button onClick={onClose} variant={ButtonVariant.Secondary}>
<Intl i18n={i18n} id="icu:ok" />
</Button>
</Modal.ButtonFooter>
</div>
);
}

View file

@ -0,0 +1,23 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { action } from '@storybook/addon-actions';
import { SafetyNumberOnboarding } from './SafetyNumberOnboarding';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
export default {
title: 'Components/SafetyNumberOnboarding',
};
export function Default(): JSX.Element {
return <SafetyNumberOnboarding i18n={i18n} onClose={action('close')} />;
}
Default.story = {
name: 'Safety Number Onboarding',
};

View file

@ -0,0 +1,78 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback, useRef } from 'react';
import Lottie from 'lottie-react';
import type { LottieRefCurrentProps } from 'lottie-react';
import { Button, ButtonVariant } from './Button';
import { Intl } from './Intl';
import type { LocalizerType } from '../types/Util';
import { SAFETY_NUMBER_MIGRATION_URL } from '../types/support';
import { useReducedMotion } from '../hooks/useReducedMotion';
import animationData from '../../images/safety-number-onboarding.json';
import reducedAnimationData from '../../images/safety-number-onboarding-reduced-motion.json';
export type PropsType = {
i18n: LocalizerType;
onClose: () => void;
};
export function SafetyNumberOnboarding({
i18n,
onClose,
}: PropsType): JSX.Element | null {
const isMotionReduced = useReducedMotion();
const lottieRef = useRef<LottieRefCurrentProps | null>(null);
const onDOMLoaded = useCallback(() => {
if (isMotionReduced) {
lottieRef.current?.goToAndPlay(0);
return;
}
lottieRef.current?.playSegments(
[
[0, 360],
[60, 360],
],
true
);
}, [isMotionReduced]);
return (
<div className="module-SafetyNumberOnboarding">
<h2>
<Intl i18n={i18n} id="icu:SafetyNumberOnboarding__title" />
</h2>
<p>
<Intl i18n={i18n} id="icu:SafetyNumberOnboarding__p1" />
</p>
<p>
<Intl i18n={i18n} id="icu:SafetyNumberOnboarding__p2" />
</p>
<Lottie
lottieRef={lottieRef}
animationData={isMotionReduced ? reducedAnimationData : animationData}
onDOMLoaded={onDOMLoaded}
/>
<div className="module-SafetyNumberOnboarding__help">
<a
key="signal-support"
href={SAFETY_NUMBER_MIGRATION_URL}
rel="noreferrer"
target="_blank"
>
<Intl i18n={i18n} id="icu:SafetyNumberOnboarding__help" />
</a>
</div>
<Button
className="module-SafetyNumberOnboarding__close"
onClick={onClose}
variant={ButtonVariant.Primary}
>
<Intl i18n={i18n} id="icu:SafetyNumberOnboarding__close" />
</Button>
</div>
);
}

View file

@ -8,9 +8,33 @@ import { boolean, text } from '@storybook/addon-knobs';
import type { PropsType } from './SafetyNumberViewer'; import type { PropsType } from './SafetyNumberViewer';
import { SafetyNumberViewer } from './SafetyNumberViewer'; import { SafetyNumberViewer } from './SafetyNumberViewer';
import { setupI18n } from '../util/setupI18n'; import { setupI18n } from '../util/setupI18n';
import {
SafetyNumberIdentifierType,
SafetyNumberMode,
} from '../types/safetyNumber';
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation'; import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
function generateQRData() {
const data = new Uint8Array(128);
for (let i = 0; i < data.length; i += 1) {
data[i] = Math.floor(Math.random() * 256);
}
return data;
}
function generateNumberBlocks() {
const result = new Array<string>();
for (let i = 0; i < 12; i += 1) {
let digits = '';
for (let j = 0; j < 5; j += 1) {
digits += Math.floor(Math.random() * 10);
}
result.push(digits);
}
return result;
}
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const contactWithAllData = getDefaultConversation({ const contactWithAllData = getDefaultConversation({
@ -49,7 +73,17 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
contact: overrideProps.contact || contactWithAllData, contact: overrideProps.contact || contactWithAllData,
generateSafetyNumber: action('generate-safety-number'), generateSafetyNumber: action('generate-safety-number'),
i18n, i18n,
safetyNumber: text('safetyNumber', overrideProps.safetyNumber || 'XXX'), safetyNumberMode: overrideProps.safetyNumberMode ?? SafetyNumberMode.ACI,
safetyNumbers: overrideProps.safetyNumbers ?? [
{
identifierType: SafetyNumberIdentifierType.ACIIdentifier,
numberBlocks: text(
'safetyNumber',
generateNumberBlocks().join(' ')
).split(' '),
qrData: generateQRData(),
},
],
toggleVerified: action('toggle-verified'), toggleVerified: action('toggle-verified'),
verificationDisabled: boolean( verificationDisabled: boolean(
'verificationDisabled', 'verificationDisabled',
@ -68,6 +102,56 @@ export function SafetyNumber(): JSX.Element {
return <SafetyNumberViewer {...createProps({})} />; return <SafetyNumberViewer {...createProps({})} />;
} }
export function SafetyNumberBeforeE164Transition(): JSX.Element {
return (
<SafetyNumberViewer
{...createProps({
safetyNumberMode: SafetyNumberMode.E164,
safetyNumbers: [
{
identifierType: SafetyNumberIdentifierType.E164Identifier,
numberBlocks: text(
'safetyNumber',
generateNumberBlocks().join(' ')
).split(' '),
qrData: generateQRData(),
},
],
})}
/>
);
}
SafetyNumberBeforeE164Transition.story = {
name: 'Safety Number (before e164 transition)',
};
export function SafetyNumberE164Transition(): JSX.Element {
return (
<SafetyNumberViewer
{...createProps({
safetyNumberMode: SafetyNumberMode.ACIAndE164,
safetyNumbers: [
{
identifierType: SafetyNumberIdentifierType.E164Identifier,
numberBlocks: generateNumberBlocks(),
qrData: generateQRData(),
},
{
identifierType: SafetyNumberIdentifierType.ACIIdentifier,
numberBlocks: generateNumberBlocks(),
qrData: generateQRData(),
},
],
})}
/>
);
}
SafetyNumberE164Transition.story = {
name: 'Safety Number (e164 transition)',
};
export function SafetyNumberNotVerified(): JSX.Element { export function SafetyNumberNotVerified(): JSX.Element {
return ( return (
<SafetyNumberViewer <SafetyNumberViewer

View file

@ -1,19 +1,31 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React, { useState } from 'react';
import classNames from 'classnames';
import { Button, ButtonVariant } from './Button'; import { Button, ButtonVariant } from './Button';
import { QrCode } from './QrCode';
import type { ConversationType } from '../state/ducks/conversations'; import type { ConversationType } from '../state/ducks/conversations';
import { Intl } from './Intl'; import { Intl } from './Intl';
import { Emojify } from './conversation/Emojify';
import type { LocalizerType } from '../types/Util'; import type { LocalizerType } from '../types/Util';
import type { SafetyNumberType } from '../types/safetyNumber';
import { SAFETY_NUMBER_MIGRATION_URL } from '../types/support';
import {
SafetyNumberIdentifierType,
SafetyNumberMode,
} from '../types/safetyNumber';
export type PropsType = { export type PropsType = {
contact: ConversationType; contact: ConversationType;
generateSafetyNumber: (contact: ConversationType) => void; generateSafetyNumber: (contact: ConversationType) => void;
i18n: LocalizerType; i18n: LocalizerType;
onClose: () => void; onClose: () => void;
safetyNumber: string; safetyNumberMode: SafetyNumberMode;
safetyNumbers?: ReadonlyArray<SafetyNumberType>;
toggleVerified: (contact: ConversationType) => void; toggleVerified: (contact: ConversationType) => void;
showOnboarding?: () => void;
verificationDisabled: boolean; verificationDisabled: boolean;
}; };
@ -22,23 +34,28 @@ export function SafetyNumberViewer({
generateSafetyNumber, generateSafetyNumber,
i18n, i18n,
onClose, onClose,
safetyNumber, safetyNumberMode,
safetyNumbers,
toggleVerified, toggleVerified,
showOnboarding,
verificationDisabled, verificationDisabled,
}: PropsType): JSX.Element | null { }: PropsType): JSX.Element | null {
const hasSafetyNumbers = safetyNumbers != null;
React.useEffect(() => { React.useEffect(() => {
if (!contact) { if (!contact) {
return; return;
} }
generateSafetyNumber(contact); generateSafetyNumber(contact);
}, [contact, generateSafetyNumber, safetyNumber]); }, [contact, generateSafetyNumber]);
if (!contact) { const [selectedIndex, setSelectedIndex] = useState(0);
if (!contact || !hasSafetyNumbers) {
return null; return null;
} }
if (!contact.phoneNumber) { if (!safetyNumbers.length) {
return ( return (
<div className="module-SafetyNumberViewer"> <div className="module-SafetyNumberViewer">
<div>{i18n('icu:cannotGenerateSafetyNumber')}</div> <div>{i18n('icu:cannotGenerateSafetyNumber')}</div>
@ -55,12 +72,10 @@ export function SafetyNumberViewer({
); );
} }
const showNumber = Boolean(contact.name || contact.profileName);
const numberFragment =
showNumber && contact.phoneNumber ? ` · ${contact.phoneNumber}` : '';
const name = `${contact.title}${numberFragment}`;
const boldName = ( const boldName = (
<span className="module-SafetyNumberViewer__bold-name">{name}</span> <span className="module-SafetyNumberViewer__bold-name">
<Emojify text={contact.title} />
</span>
); );
const { isVerified } = contact; const { isVerified } = contact;
@ -68,32 +83,136 @@ export function SafetyNumberViewer({
? i18n('icu:SafetyNumberViewer__clearVerification') ? i18n('icu:SafetyNumberViewer__clearVerification')
: i18n('icu:SafetyNumberViewer__markAsVerified'); : i18n('icu:SafetyNumberViewer__markAsVerified');
const isMigrationVisible = safetyNumberMode === SafetyNumberMode.ACIAndE164;
const visibleSafetyNumber = safetyNumbers.at(selectedIndex);
if (!visibleSafetyNumber) {
return null;
}
const cardClassName = classNames('module-SafetyNumberViewer__card', {
'module-SafetyNumberViewer__card--aci':
visibleSafetyNumber.identifierType ===
SafetyNumberIdentifierType.ACIIdentifier,
'module-SafetyNumberViewer__card--e164':
visibleSafetyNumber.identifierType ===
SafetyNumberIdentifierType.E164Identifier,
});
const numberBlocks = visibleSafetyNumber.numberBlocks.join(' ');
const safetyNumberCard = (
<div className="module-SafetyNumberViewer__card-container">
<div className={cardClassName}>
<QrCode
className="module-SafetyNumberViewer__card__qr"
data={visibleSafetyNumber.qrData}
alt={i18n('icu:Install__scan-this-code')}
/>
<div className="module-SafetyNumberViewer__card__number">
{numberBlocks}
</div>
{selectedIndex > 0 && (
<button
type="button"
aria-label={i18n('icu:SafetyNumberViewer__card__prev')}
className="module-SafetyNumberViewer__card__prev"
onClick={() => setSelectedIndex(x => x - 1)}
/>
)}
{selectedIndex < safetyNumbers.length - 1 && (
<button
type="button"
aria-label={i18n('icu:SafetyNumberViewer__card__next')}
className="module-SafetyNumberViewer__card__next"
onClick={() => setSelectedIndex(x => x + 1)}
/>
)}
</div>
</div>
);
const carousel = (
<div className="module-SafetyNumberViewer__carousel">
{safetyNumbers.map(({ identifierType }, index) => {
return (
<button
type="button"
aria-label={i18n('icu:SafetyNumberViewer__carousel__dot', {
index: index + 1,
total: safetyNumbers.length,
})}
aria-pressed={index === selectedIndex}
key={identifierType}
className="module-SafetyNumberViewer__carousel__dot"
onClick={() => setSelectedIndex(index)}
/>
);
})}
</div>
);
return ( return (
<div className="module-SafetyNumberViewer"> <div className="module-SafetyNumberViewer">
<div className="module-SafetyNumberViewer__number"> {isMigrationVisible && (
{safetyNumber || getPlaceholder()} <div className="module-SafetyNumberViewer__migration">
</div> <div className="module-SafetyNumberViewer__migration__icon" />
<Intl i18n={i18n} id="icu:verifyHelp" components={{ name: boldName }} />
<div className="module-SafetyNumberViewer__verification-status"> <div className="module-SafetyNumberViewer__migration__text">
{isVerified ? ( <p>
<span className="module-SafetyNumberViewer__icon--verified" /> <Intl i18n={i18n} id="icu:SafetyNumberViewer__migration__text" />
) : ( </p>
<span className="module-SafetyNumberViewer__icon--shield" /> <p>
)} <a
{isVerified ? ( href={SAFETY_NUMBER_MIGRATION_URL}
rel="noreferrer"
target="_blank"
onClick={e => {
if (showOnboarding) {
e.preventDefault();
showOnboarding();
}
}}
>
<Intl <Intl
i18n={i18n} i18n={i18n}
id="icu:isVerified" id="icu:SafetyNumberViewer__migration__learn_more"
/>
</a>
</p>
</div>
</div>
)}
{safetyNumberCard}
{safetyNumbers.length > 1 && carousel}
<div className="module-SafetyNumberViewer__help">
{isMigrationVisible ? (
<Intl
i18n={i18n}
id="icu:SafetyNumberViewer__hint--migration"
components={{ name: boldName }} components={{ name: boldName }}
/> />
) : ( ) : (
<Intl <Intl
i18n={i18n} i18n={i18n}
id="icu:isNotVerified" id="icu:SafetyNumberViewer__hint--normal"
components={{ name: boldName }} components={{ name: boldName }}
/> />
)} )}
<br />
<a href={SAFETY_NUMBER_MIGRATION_URL} rel="noreferrer" target="_blank">
<Intl
i18n={i18n}
id="icu:SafetyNumberViewer__migration__learn_more"
/>
</a>
</div> </div>
<div className="module-SafetyNumberViewer__button"> <div className="module-SafetyNumberViewer__button">
<Button <Button
disabled={verificationDisabled} disabled={verificationDisabled}
@ -108,9 +227,3 @@ export function SafetyNumberViewer({
</div> </div>
); );
} }
function getPlaceholder(): string {
return Array.from(Array(12))
.map(() => 'XXXXX')
.join(' ');
}

View file

@ -2915,6 +2915,10 @@ export class ConversationModel extends window.Backbone
window.reduxActions.calling.keyChanged({ uuid }); window.reduxActions.calling.keyChanged({ uuid });
} }
if (isDirectConversation(this.attributes)) {
window.reduxActions?.safetyNumber.clearSafetyNumber(this.id);
}
if (isDirectConversation(this.attributes) && uuid) { if (isDirectConversation(this.attributes) && uuid) {
const parsedUuid = UUID.checkedLookup(uuid); const parsedUuid = UUID.checkedLookup(uuid);
const groups = const groups =

View file

@ -30,6 +30,7 @@ export type ItemsStateType = ReadonlyDeep<{
[key: string]: unknown; [key: string]: unknown;
remoteConfig?: RemoteConfigType; remoteConfig?: RemoteConfigType;
serverTimeSkew?: number;
// This property should always be set and this is ensured in background.ts // This property should always be set and this is ensured in background.ts
defaultConversationColor?: DefaultConversationColorType; defaultConversationColor?: DefaultConversationColorType;
@ -85,6 +86,7 @@ export type ItemsActionType = ReadonlyDeep<
export const actions = { export const actions = {
addCustomColor, addCustomColor,
editCustomColor, editCustomColor,
markHasCompletedSafetyNumberOnboarding,
removeCustomColor, removeCustomColor,
resetDefaultChatColor, resetDefaultChatColor,
savePreferredLeftPaneWidth, savePreferredLeftPaneWidth,
@ -280,6 +282,17 @@ function savePreferredLeftPaneWidth(
}; };
} }
function markHasCompletedSafetyNumberOnboarding(): ThunkAction<
void,
RootStateType,
unknown,
ItemPutAction
> {
return dispatch => {
dispatch(putItem('hasCompletedSafetyNumberOnboarding', true));
};
}
// Reducer // Reducer
export function getEmptyState(): ItemsStateType { export function getEmptyState(): ItemsStateType {

View file

@ -3,8 +3,10 @@
import type { ReadonlyDeep } from 'type-fest'; import type { ReadonlyDeep } from 'type-fest';
import type { ThunkAction } from 'redux-thunk'; import type { ThunkAction } from 'redux-thunk';
import { omit } from 'lodash';
import { generateSecurityNumberBlock } from '../../util/safetyNumber'; import { generateSafetyNumbers } from '../../util/safetyNumber';
import type { SafetyNumberType } from '../../types/safetyNumber';
import type { ConversationType } from './conversations'; import type { ConversationType } from './conversations';
import { import {
reloadProfiles, reloadProfiles,
@ -13,10 +15,10 @@ import {
import * as log from '../../logging/log'; import * as log from '../../logging/log';
import * as Errors from '../../types/errors'; import * as Errors from '../../types/errors';
import type { StateType as RootStateType } from '../reducer'; import type { StateType as RootStateType } from '../reducer';
import { getSecurityNumberIdentifierType } from '../selectors/items'; import { getSafetyNumberMode } from '../selectors/items';
export type SafetyNumberContactType = ReadonlyDeep<{ export type SafetyNumberContactType = ReadonlyDeep<{
safetyNumber: string; safetyNumbers: ReadonlyArray<SafetyNumberType>;
safetyNumberChanged?: boolean; safetyNumberChanged?: boolean;
verificationDisabled: boolean; verificationDisabled: boolean;
}>; }>;
@ -27,15 +29,23 @@ export type SafetyNumberStateType = ReadonlyDeep<{
}; };
}>; }>;
const CLEAR_SAFETY_NUMBER = 'safetyNumber/CLEAR_SAFETY_NUMBER';
const GENERATE_FULFILLED = 'safetyNumber/GENERATE_FULFILLED'; const GENERATE_FULFILLED = 'safetyNumber/GENERATE_FULFILLED';
const TOGGLE_VERIFIED_FULFILLED = 'safetyNumber/TOGGLE_VERIFIED_FULFILLED'; const TOGGLE_VERIFIED_FULFILLED = 'safetyNumber/TOGGLE_VERIFIED_FULFILLED';
const TOGGLE_VERIFIED_PENDING = 'safetyNumber/TOGGLE_VERIFIED_PENDING'; const TOGGLE_VERIFIED_PENDING = 'safetyNumber/TOGGLE_VERIFIED_PENDING';
type ClearSafetyNumberActionType = ReadonlyDeep<{
type: 'safetyNumber/CLEAR_SAFETY_NUMBER';
payload: {
contactId: string;
};
}>;
type GenerateFulfilledActionType = ReadonlyDeep<{ type GenerateFulfilledActionType = ReadonlyDeep<{
type: 'safetyNumber/GENERATE_FULFILLED'; type: 'safetyNumber/GENERATE_FULFILLED';
payload: { payload: {
contact: ConversationType; contact: ConversationType;
safetyNumber: string; safetyNumbers: ReadonlyArray<SafetyNumberType>;
}; };
}>; }>;
@ -50,31 +60,39 @@ type ToggleVerifiedFulfilledActionType = ReadonlyDeep<{
type: 'safetyNumber/TOGGLE_VERIFIED_FULFILLED'; type: 'safetyNumber/TOGGLE_VERIFIED_FULFILLED';
payload: { payload: {
contact: ConversationType; contact: ConversationType;
safetyNumber?: string; safetyNumbers?: ReadonlyArray<SafetyNumberType>;
safetyNumberChanged?: boolean; safetyNumberChanged?: boolean;
}; };
}>; }>;
export type SafetyNumberActionType = ReadonlyDeep< export type SafetyNumberActionType = ReadonlyDeep<
| ClearSafetyNumberActionType
| GenerateFulfilledActionType | GenerateFulfilledActionType
| ToggleVerifiedPendingActionType | ToggleVerifiedPendingActionType
| ToggleVerifiedFulfilledActionType | ToggleVerifiedFulfilledActionType
>; >;
function clearSafetyNumber(contactId: string): ClearSafetyNumberActionType {
return {
type: CLEAR_SAFETY_NUMBER,
payload: { contactId },
};
}
function generate( function generate(
contact: ConversationType contact: ConversationType
): ThunkAction<void, RootStateType, unknown, GenerateFulfilledActionType> { ): ThunkAction<void, RootStateType, unknown, GenerateFulfilledActionType> {
return async (dispatch, getState) => { return async (dispatch, getState) => {
try { try {
const securityNumberBlock = await generateSecurityNumberBlock( const safetyNumbers = await generateSafetyNumbers(
contact, contact,
getSecurityNumberIdentifierType(getState(), { now: Date.now() }) getSafetyNumberMode(getState(), { now: Date.now() })
); );
dispatch({ dispatch({
type: GENERATE_FULFILLED, type: GENERATE_FULFILLED,
payload: { payload: {
contact, contact,
safetyNumber: securityNumberBlock.join(' '), safetyNumbers,
}, },
}); });
} catch (error) { } catch (error) {
@ -114,16 +132,16 @@ function toggleVerified(
} catch (err) { } catch (err) {
if (err.name === 'OutgoingIdentityKeyError') { if (err.name === 'OutgoingIdentityKeyError') {
await reloadProfiles(contact.id); await reloadProfiles(contact.id);
const securityNumberBlock = await generateSecurityNumberBlock( const safetyNumbers = await generateSafetyNumbers(
contact, contact,
getSecurityNumberIdentifierType(getState(), { now: Date.now() }) getSafetyNumberMode(getState(), { now: Date.now() })
); );
dispatch({ dispatch({
type: TOGGLE_VERIFIED_FULFILLED, type: TOGGLE_VERIFIED_FULFILLED,
payload: { payload: {
contact, contact,
safetyNumber: securityNumberBlock.join(' '), safetyNumbers,
safetyNumberChanged: true, safetyNumberChanged: true,
}, },
}); });
@ -158,6 +176,7 @@ async function alterVerification(contact: ConversationType): Promise<void> {
} }
export const actions = { export const actions = {
clearSafetyNumber,
generateSafetyNumber: generate, generateSafetyNumber: generate,
toggleVerified, toggleVerified,
}; };
@ -172,6 +191,13 @@ export function reducer(
state: Readonly<SafetyNumberStateType> = getEmptyState(), state: Readonly<SafetyNumberStateType> = getEmptyState(),
action: Readonly<SafetyNumberActionType> action: Readonly<SafetyNumberActionType>
): SafetyNumberStateType { ): SafetyNumberStateType {
if (action.type === CLEAR_SAFETY_NUMBER) {
const { contactId } = action.payload;
return {
contacts: omit(state.contacts, contactId),
};
}
if (action.type === TOGGLE_VERIFIED_PENDING) { if (action.type === TOGGLE_VERIFIED_PENDING) {
const { contact } = action.payload; const { contact } = action.payload;
const { id } = contact; const { id } = contact;
@ -205,7 +231,7 @@ export function reducer(
} }
if (action.type === GENERATE_FULFILLED) { if (action.type === GENERATE_FULFILLED) {
const { contact, safetyNumber } = action.payload; const { contact, safetyNumbers } = action.payload;
const { id } = contact; const { id } = contact;
const record = state.contacts[id]; const record = state.contacts[id];
return { return {
@ -213,7 +239,7 @@ export function reducer(
...state.contacts, ...state.contacts,
[id]: { [id]: {
...record, ...record,
safetyNumber, safetyNumbers,
}, },
}, },
}; };

View file

@ -5,7 +5,7 @@ import { createSelector } from 'reselect';
import { isInteger } from 'lodash'; import { isInteger } from 'lodash';
import { ITEM_NAME as UNIVERSAL_EXPIRE_TIMER_ITEM } from '../../util/universalExpireTimer'; import { ITEM_NAME as UNIVERSAL_EXPIRE_TIMER_ITEM } from '../../util/universalExpireTimer';
import { SecurityNumberIdentifierType } from '../../util/safetyNumber'; import { SafetyNumberMode } from '../../types/safetyNumber';
import { innerIsBucketValueEnabled } from '../../RemoteConfig'; import { innerIsBucketValueEnabled } from '../../RemoteConfig';
import type { ConfigKeyType, ConfigMapType } from '../../RemoteConfig'; import type { ConfigKeyType, ConfigMapType } from '../../RemoteConfig';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
@ -69,6 +69,11 @@ export const getRemoteConfig = createSelector(
(state: ItemsStateType): ConfigMapType => state.remoteConfig || {} (state: ItemsStateType): ConfigMapType => state.remoteConfig || {}
); );
export const getServerTimeSkew = createSelector(
getItems,
(state: ItemsStateType): number => state.serverTimeSkew || 0
);
export const getUsernamesEnabled = createSelector( export const getUsernamesEnabled = createSelector(
getRemoteConfig, getRemoteConfig,
(remoteConfig: ConfigMapType): boolean => (remoteConfig: ConfigMapType): boolean =>
@ -81,6 +86,12 @@ export const getHasCompletedUsernameOnboarding = createSelector(
Boolean(state.hasCompletedUsernameOnboarding) Boolean(state.hasCompletedUsernameOnboarding)
); );
export const getHasCompletedSafetyNumberOnboarding = createSelector(
getItems,
(state: ItemsStateType): boolean =>
Boolean(state.hasCompletedSafetyNumberOnboarding)
);
export const isInternalUser = createSelector( export const isInternalUser = createSelector(
getRemoteConfig, getRemoteConfig,
(remoteConfig: ConfigMapType): boolean => { (remoteConfig: ConfigMapType): boolean => {
@ -146,22 +157,29 @@ export const getContactManagementEnabled = createSelector(
} }
); );
export const getSecurityNumberIdentifierType = createSelector( export const getSafetyNumberMode = createSelector(
getRemoteConfig, getRemoteConfig,
getServerTimeSkew,
(_state: StateType, { now }: { now: number }) => now, (_state: StateType, { now }: { now: number }) => now,
(remoteConfig: ConfigMapType, now: number): SecurityNumberIdentifierType => { (
if (isRemoteConfigFlagEnabled(remoteConfig, 'desktop.safetyNumberUUID')) { remoteConfig: ConfigMapType,
return SecurityNumberIdentifierType.UUIDIdentifier; serverTimeSkew: number,
now: number
): SafetyNumberMode => {
if (!isRemoteConfigFlagEnabled(remoteConfig, 'desktop.safetyNumberAci')) {
return SafetyNumberMode.E164;
} }
const timestamp = remoteConfig['desktop.safetyNumberUUID.timestamp']?.value; const timestamp = remoteConfig['global.safetyNumberAci']?.value;
if (typeof timestamp !== 'number') { if (typeof timestamp !== 'number') {
return SecurityNumberIdentifierType.E164Identifier; return SafetyNumberMode.ACIAndE164;
} }
return now >= timestamp // Note: serverTimeSkew is a difference between server time and local time,
? SecurityNumberIdentifierType.UUIDIdentifier // so we have to add local time to it to correct it for a skew.
: SecurityNumberIdentifierType.E164Identifier; return now + serverTimeSkew >= timestamp
? SafetyNumberMode.ACI
: SafetyNumberMode.ACIAndE164;
} }
); );

View file

@ -7,6 +7,10 @@ import { SafetyNumberModal } from '../../components/SafetyNumberModal';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import { getContactSafetyNumber } from '../selectors/safetyNumber'; import { getContactSafetyNumber } from '../selectors/safetyNumber';
import { getConversationSelector } from '../selectors/conversations'; import { getConversationSelector } from '../selectors/conversations';
import {
getSafetyNumberMode,
getHasCompletedSafetyNumberOnboarding,
} from '../selectors/items';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
export type Props = { export type Props = {
@ -18,6 +22,9 @@ const mapStateToProps = (state: StateType, props: Props) => {
...props, ...props,
...getContactSafetyNumber(state, props), ...getContactSafetyNumber(state, props),
contact: getConversationSelector(state)(props.contactID), contact: getConversationSelector(state)(props.contactID),
safetyNumberMode: getSafetyNumberMode(state, { now: Date.now() }),
hasCompletedSafetyNumberOnboarding:
getHasCompletedSafetyNumberOnboarding(state),
i18n: getIntl(state), i18n: getIntl(state),
}; };
}; };

View file

@ -8,6 +8,7 @@ import type { StateType } from '../reducer';
import type { SafetyNumberProps } from '../../components/SafetyNumberChangeDialog'; import type { SafetyNumberProps } from '../../components/SafetyNumberChangeDialog';
import { getContactSafetyNumber } from '../selectors/safetyNumber'; import { getContactSafetyNumber } from '../selectors/safetyNumber';
import { getConversationSelector } from '../selectors/conversations'; import { getConversationSelector } from '../selectors/conversations';
import { getSafetyNumberMode } from '../selectors/items';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
const mapStateToProps = (state: StateType, props: SafetyNumberProps) => { const mapStateToProps = (state: StateType, props: SafetyNumberProps) => {
@ -15,6 +16,7 @@ const mapStateToProps = (state: StateType, props: SafetyNumberProps) => {
...props, ...props,
...getContactSafetyNumber(state, props), ...getContactSafetyNumber(state, props),
contact: getConversationSelector(state)(props.contactID), contact: getConversationSelector(state)(props.contactID),
safetyNumberMode: getSafetyNumberMode(state, { now: Date.now() }),
i18n: getIntl(state), i18n: getIntl(state),
}; };
}; };

View file

@ -2,15 +2,18 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { refreshRemoteConfig } from '../../RemoteConfig'; import { refreshRemoteConfig } from '../../RemoteConfig';
import type { WebAPIType } from '../../textsecure/WebAPI'; import type {
import type { UnwrapPromise } from '../../types/Util'; WebAPIType,
RemoteConfigResponseType,
} from '../../textsecure/WebAPI';
import { SECOND } from '../../util/durations';
export async function updateRemoteConfig( export async function updateRemoteConfig(
newConfig: UnwrapPromise<ReturnType<WebAPIType['getConfig']>> newConfig: RemoteConfigResponseType['config']
): Promise<void> { ): Promise<void> {
const fakeServer = { const fakeServer = {
async getConfig() { async getConfig() {
return newConfig; return { config: newConfig, serverEpochTime: Date.now() / SECOND };
}, },
} as Partial<WebAPIType> as unknown as WebAPIType; } as Partial<WebAPIType> as unknown as WebAPIType;

View file

@ -686,6 +686,18 @@ const uploadAvatarHeadersZod = z.object({
}); });
export type UploadAvatarHeadersType = z.infer<typeof uploadAvatarHeadersZod>; export type UploadAvatarHeadersType = z.infer<typeof uploadAvatarHeadersZod>;
const remoteConfigResponseZod = z.object({
config: z
.object({
name: z.string(),
enabled: z.boolean(),
value: z.string().or(z.null()).optional(),
})
.array(),
serverEpochTime: z.number(),
});
export type RemoteConfigResponseType = z.infer<typeof remoteConfigResponseZod>;
export type ProfileType = Readonly<{ export type ProfileType = Readonly<{
identityKey?: string; identityKey?: string;
name?: string; name?: string;
@ -1035,9 +1047,7 @@ export type WebAPIType = {
) => Promise<string>; ) => Promise<string>;
whoami: () => Promise<WhoamiResultType>; whoami: () => Promise<WhoamiResultType>;
sendChallengeResponse: (challengeResponse: ChallengeType) => Promise<void>; sendChallengeResponse: (challengeResponse: ChallengeType) => Promise<void>;
getConfig: () => Promise< getConfig: () => Promise<RemoteConfigResponseType>;
Array<{ name: string; enabled: boolean; value: string | null }>
>;
authenticate: (credentials: WebAPICredentials) => Promise<void>; authenticate: (credentials: WebAPICredentials) => Promise<void>;
logout: () => Promise<void>; logout: () => Promise<void>;
getSocketStatus: () => SocketStatus; getSocketStatus: () => SocketStatus;
@ -1488,19 +1498,20 @@ export function initialize({
} }
async function getConfig() { async function getConfig() {
type ResType = { const rawRes = await _ajax({
config: Array<{ name: string; enabled: boolean; value: string | null }>;
};
const res = (await _ajax({
call: 'config', call: 'config',
httpType: 'GET', httpType: 'GET',
responseType: 'json', responseType: 'json',
})) as ResType; });
const res = remoteConfigResponseZod.parse(rawRes);
return res.config.filter( return {
...res,
config: res.config.filter(
({ name }: { name: string }) => ({ name }: { name: string }) =>
name.startsWith('desktop.') || name.startsWith('global.') name.startsWith('desktop.') || name.startsWith('global.')
); ),
};
} }
async function getSenderCertificate(omitE164?: boolean) { async function getSenderCertificate(omitE164?: boolean) {

View file

@ -74,6 +74,7 @@ export type StorageAccessType = {
hasRegisterSupportForUnauthenticatedDelivery: boolean; hasRegisterSupportForUnauthenticatedDelivery: boolean;
hasSetMyStoriesPrivacy: boolean; hasSetMyStoriesPrivacy: boolean;
hasCompletedUsernameOnboarding: boolean; hasCompletedUsernameOnboarding: boolean;
hasCompletedSafetyNumberOnboarding: boolean;
hasViewedOnboardingStory: boolean; hasViewedOnboardingStory: boolean;
hasStoriesDisabled: boolean; hasStoriesDisabled: boolean;
storyViewReceiptsEnabled: boolean; storyViewReceiptsEnabled: boolean;
@ -128,6 +129,7 @@ export type StorageAccessType = {
'preferred-audio-output-device': AudioDevice; 'preferred-audio-output-device': AudioDevice;
previousAudioDeviceModule: AudioDeviceModule; previousAudioDeviceModule: AudioDeviceModule;
remoteConfig: RemoteConfigType; remoteConfig: RemoteConfigType;
serverTimeSkew: number;
unidentifiedDeliveryIndicators: boolean; unidentifiedDeliveryIndicators: boolean;
groupCredentials: ReadonlyArray<GroupCredentialType>; groupCredentials: ReadonlyArray<GroupCredentialType>;
lastReceivedAtCounter: number; lastReceivedAtCounter: number;

19
ts/types/safetyNumber.ts Normal file
View file

@ -0,0 +1,19 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export enum SafetyNumberMode {
E164 = 'E164',
ACIAndE164 = 'ACIAndE164',
ACI = 'ACI',
}
export enum SafetyNumberIdentifierType {
ACIIdentifier = 'ACIIdentifier',
E164Identifier = 'E164Identifier',
}
export type SafetyNumberType = Readonly<{
identifierType: SafetyNumberIdentifierType;
numberBlocks: ReadonlyArray<string>;
qrData: Uint8Array;
}>;

View file

@ -7,3 +7,5 @@ export const UNSUPPORTED_OS_URL =
'https://support.signal.org/hc/articles/5109141421850'; 'https://support.signal.org/hc/articles/5109141421850';
export const LINK_SIGNAL_DESKTOP = export const LINK_SIGNAL_DESKTOP =
'https://support.signal.org/hc/articles/360007320451#desktop_multiple_device'; 'https://support.signal.org/hc/articles/360007320451#desktop_multiple_device';
export const SAFETY_NUMBER_MIGRATION_URL =
'https://support.signal.org/hc/en-us/articles/360007060632';

View file

@ -0,0 +1,19 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ConversationType } from '../state/ducks/conversations';
export const isSafetyNumberNotAvailable = (
contact?: ConversationType
): boolean => {
// We have a contact
if (!contact) {
return true;
}
// They have a uuid
if (!contact.uuid) {
return true;
}
// The uuid is not PNI
return contact.pni === contact.uuid;
};

View file

@ -905,6 +905,234 @@
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2021-04-05T20:48:36.065Z" "updated": "2021-04-05T20:48:36.065Z"
}, },
{
"rule": "React-useRef",
"path": "node_modules/lottie-react/build/index.es.js",
"line": " var animationInstanceRef = useRef();",
"reasonCategory": "usageTrusted",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "React-useRef",
"path": "node_modules/lottie-react/build/index.es.js",
"line": " var animationContainer = useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "React-useRef",
"path": "node_modules/lottie-react/build/index.js",
"line": " var animationInstanceRef = React.useRef();",
"reasonCategory": "usageTrusted",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "React-useRef",
"path": "node_modules/lottie-react/build/index.js",
"line": " var animationContainer = React.useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "React-useRef",
"path": "node_modules/lottie-react/build/index.min.js",
"reasonCategory": "usageTrusted",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "React-useRef",
"path": "node_modules/lottie-react/build/index.umd.js",
"line": " var animationInstanceRef = React.useRef();",
"reasonCategory": "usageTrusted",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "React-useRef",
"path": "node_modules/lottie-react/build/index.umd.js",
"line": " var animationContainer = React.useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "React-useRef",
"path": "node_modules/lottie-react/build/index.umd.min.js",
"reasonCategory": "usageTrusted",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "eval",
"path": "node_modules/lottie-web/build/player/cjs/lottie.min.js",
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "eval",
"path": "node_modules/lottie-web/build/player/cjs/lottie_canvas.min.js",
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "eval",
"path": "node_modules/lottie-web/build/player/cjs/lottie_html.min.js",
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "eval",
"path": "node_modules/lottie-web/build/player/cjs/lottie_svg.min.js",
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "eval",
"path": "node_modules/lottie-web/build/player/esm/lottie.min.js",
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "eval",
"path": "node_modules/lottie-web/build/player/esm/lottie_canvas.min.js",
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "eval",
"path": "node_modules/lottie-web/build/player/esm/lottie_html.min.js",
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "eval",
"path": "node_modules/lottie-web/build/player/esm/lottie_svg.min.js",
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "eval",
"path": "node_modules/lottie-web/build/player/lottie.js",
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "eval",
"path": "node_modules/lottie-web/build/player/lottie.min.js",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "eval",
"path": "node_modules/lottie-web/build/player/lottie_canvas.js",
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "eval",
"path": "node_modules/lottie-web/build/player/lottie_canvas.min.js",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/lottie-web/build/player/lottie_canvas_worker.js",
"line": " animation.container.innerHTML = '';",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "eval",
"path": "node_modules/lottie-web/build/player/lottie_canvas_worker.js",
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/lottie-web/build/player/lottie_canvas_worker.min.js",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "eval",
"path": "node_modules/lottie-web/build/player/lottie_canvas_worker.min.js",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "eval",
"path": "node_modules/lottie-web/build/player/lottie_html.js",
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "eval",
"path": "node_modules/lottie-web/build/player/lottie_html.min.js",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "eval",
"path": "node_modules/lottie-web/build/player/lottie_svg.js",
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "eval",
"path": "node_modules/lottie-web/build/player/lottie_svg.min.js",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/lottie-web/build/player/lottie_worker.js",
"line": " animation.container.innerHTML = '';",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "eval",
"path": "node_modules/lottie-web/build/player/lottie_worker.js",
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/lottie-web/build/player/lottie_worker.min.js",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "eval",
"path": "node_modules/lottie-web/build/player/lottie_worker.min.js",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "eval",
"path": "node_modules/lottie-web/player/js/utils/expressions/ExpressionManager.js",
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/lottie-web/player/js/worker_wrapper.js",
"line": " animation.container.innerHTML = '';",
"reasonCategory": "notExercisedByOurApp",
"updated": "2023-06-29T17:01:25.145Z"
},
{ {
"rule": "DOM-innerHTML", "rule": "DOM-innerHTML",
"path": "node_modules/min-document/serialize.js", "path": "node_modules/min-document/serialize.js",
@ -2214,6 +2442,13 @@
"updated": "2022-01-04T21:43:17.517Z", "updated": "2022-01-04T21:43:17.517Z",
"reasonDetail": "Used to change the style in non-production builds." "reasonDetail": "Used to change the style in non-production builds."
}, },
{
"rule": "React-useRef",
"path": "ts/components/SafetyNumberOnboarding.tsx",
"line": " const lottieRef = useRef<LottieRefCurrentProps | null>(null);",
"reasonCategory": "usageTrusted",
"updated": "2023-06-29T17:01:25.145Z"
},
{ {
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/components/Slider.tsx", "path": "ts/components/Slider.tsx",

View file

@ -3,100 +3,117 @@
import { PublicKey, Fingerprint } from '@signalapp/libsignal-client'; import { PublicKey, Fingerprint } from '@signalapp/libsignal-client';
import type { ConversationType } from '../state/ducks/conversations'; import type { ConversationType } from '../state/ducks/conversations';
import { UUID } from '../types/UUID'; import { UUID, UUIDKind } from '../types/UUID';
import { assertDev } from './assert'; import { assertDev } from './assert';
import { isNotNil } from './isNotNil';
import { missingCaseError } from './missingCaseError'; import { missingCaseError } from './missingCaseError';
import { uuidToBytes } from './uuidToBytes';
import * as log from '../logging/log'; import * as log from '../logging/log';
import * as Bytes from '../Bytes';
import type { SafetyNumberType } from '../types/safetyNumber';
import {
SafetyNumberIdentifierType,
SafetyNumberMode,
} from '../types/safetyNumber';
function generateSecurityNumber( const ITERATION_COUNT = 5200;
ourId: string, const E164_VERSION = 1;
ourKey: Uint8Array, const UUID_VERSION = 2;
theirId: string,
theirKey: Uint8Array
): string {
const ourNumberBuf = Buffer.from(ourId);
const ourKeyObj = PublicKey.deserialize(Buffer.from(ourKey));
const theirNumberBuf = Buffer.from(theirId);
const theirKeyObj = PublicKey.deserialize(Buffer.from(theirKey));
const fingerprint = Fingerprint.new( // Number of digits in a safety number block
5200, const BLOCK_SIZE = 5;
2,
ourNumberBuf,
ourKeyObj,
theirNumberBuf,
theirKeyObj
);
return fingerprint.displayableFingerprint().toString(); export async function generateSafetyNumbers(
}
export enum SecurityNumberIdentifierType {
UUIDIdentifier = 'UUIDIdentifier',
E164Identifier = 'E164Identifier',
}
export async function generateSecurityNumberBlock(
contact: ConversationType, contact: ConversationType,
identifierType: SecurityNumberIdentifierType mode: SafetyNumberMode
): Promise<Array<string>> { ): Promise<ReadonlyArray<SafetyNumberType>> {
const logId = `generateSecurityNumberBlock(${contact.id}, ${identifierType})`; const logId = `generateSafetyNumbers(${contact.id}, ${mode})`;
log.info(`${logId}: starting`); log.info(`${logId}: starting`);
const { storage } = window.textsecure; const { storage } = window.textsecure;
const ourNumber = storage.user.getNumber(); const ourNumber = storage.user.getNumber();
const ourUuid = storage.user.getCheckedUuid(); const ourAci = storage.user.getCheckedUuid(UUIDKind.ACI);
const us = storage.protocol.getIdentityRecord(ourUuid); const us = storage.protocol.getIdentityRecord(ourAci);
const ourKey = us ? us.publicKey : null; const ourKeyBuffer = us ? us.publicKey : null;
const theirUuid = UUID.lookup(contact.id); const theirAci = contact.pni !== contact.uuid ? contact.uuid : undefined;
const them = theirUuid const them = theirAci
? await storage.protocol.getOrMigrateIdentityRecord(theirUuid) ? await storage.protocol.getOrMigrateIdentityRecord(new UUID(theirAci))
: undefined; : undefined;
const theirKey = them?.publicKey; const theirKeyBuffer = them?.publicKey;
if (!ourKey) { if (!ourKeyBuffer) {
throw new Error('Could not load our key'); throw new Error('Could not load our key');
} }
if (!theirKey) { if (!theirKeyBuffer) {
throw new Error('Could not load their key'); throw new Error('Could not load their key');
} }
let securityNumber: string; const ourKey = PublicKey.deserialize(Buffer.from(ourKeyBuffer));
if (identifierType === SecurityNumberIdentifierType.E164Identifier) { const theirKey = PublicKey.deserialize(Buffer.from(theirKeyBuffer));
let identifierTypes: ReadonlyArray<SafetyNumberIdentifierType>;
if (mode === SafetyNumberMode.ACIAndE164) {
// Important: order matters, legacy safety number should be displayed first.
identifierTypes = [
SafetyNumberIdentifierType.E164Identifier,
SafetyNumberIdentifierType.ACIIdentifier,
];
// Controlled by 'desktop.safetyNumberAci'
} else if (mode === SafetyNumberMode.E164) {
identifierTypes = [SafetyNumberIdentifierType.E164Identifier];
} else {
assertDev(mode === SafetyNumberMode.ACI, 'Invalid security number mode');
identifierTypes = [SafetyNumberIdentifierType.ACIIdentifier];
}
return identifierTypes
.map(identifierType => {
let fingerprint: Fingerprint;
if (identifierType === SafetyNumberIdentifierType.E164Identifier) {
if (!contact.e164) { if (!contact.e164) {
log.error( log.error(
`${logId}: Attempted to generate security number for contact with no e164` `${logId}: Attempted to generate security number for contact with no e164`
); );
return []; return undefined;
} }
assertDev(ourNumber, 'Should have our number'); assertDev(ourNumber, 'Should have our number');
securityNumber = generateSecurityNumber( fingerprint = Fingerprint.new(
ourNumber, ITERATION_COUNT,
E164_VERSION,
Buffer.from(Bytes.fromString(ourNumber)),
ourKey, ourKey,
contact.e164, Buffer.from(Bytes.fromString(contact.e164)),
theirKey theirKey
); );
} else if (identifierType === SecurityNumberIdentifierType.UUIDIdentifier) { } else if (identifierType === SafetyNumberIdentifierType.ACIIdentifier) {
assertDev(theirUuid, 'Should have their uuid'); assertDev(theirAci, 'Should have their uuid');
securityNumber = generateSecurityNumber( fingerprint = Fingerprint.new(
ourUuid.toString(), ITERATION_COUNT,
UUID_VERSION,
Buffer.from(uuidToBytes(ourAci.toString())),
ourKey, ourKey,
theirUuid.toString(), Buffer.from(uuidToBytes(theirAci)),
theirKey theirKey
); );
} else { } else {
throw missingCaseError(identifierType); throw missingCaseError(identifierType);
} }
const chunks = []; const securityNumber = fingerprint.displayableFingerprint().toString();
for (let i = 0; i < securityNumber.length; i += 5) {
chunks.push(securityNumber.substring(i, i + 5)); const numberBlocks = [];
for (let i = 0; i < securityNumber.length; i += BLOCK_SIZE) {
numberBlocks.push(securityNumber.substring(i, i + BLOCK_SIZE));
} }
return chunks; const qrData = fingerprint.scannableFingerprint().toBuffer();
return { identifierType, numberBlocks, qrData };
})
.filter(isNotNil);
} }

View file

@ -12457,6 +12457,18 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3
dependencies: dependencies:
js-tokens "^3.0.0 || ^4.0.0" js-tokens "^3.0.0 || ^4.0.0"
lottie-react@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/lottie-react/-/lottie-react-2.4.0.tgz#f7249eee2b1deee70457a2d142194fdf2456e4bd"
integrity sha512-pDJGj+AQlnlyHvOHFK7vLdsDcvbuqvwPZdMlJ360wrzGFurXeKPr8SiRCjLf3LrNYKANQtSsh5dz9UYQHuqx4w==
dependencies:
lottie-web "^5.10.2"
lottie-web@^5.10.2:
version "5.12.2"
resolved "https://registry.yarnpkg.com/lottie-web/-/lottie-web-5.12.2.tgz#579ca9fe6d3fd9e352571edd3c0be162492f68e5"
integrity sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==
loud-rejection@^1.0.0: loud-rejection@^1.0.0:
version "1.6.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"