Merge pull request #949 from Mikunj/auto-update

Auto update
pull/950/head
Mikunj Varsani 5 years ago committed by GitHub
commit 1996795c9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,24 @@
# Releasing
Creating a new Session Desktop release is very simple.
1. Bump up the version in `package.json`.
2. Merge all changes required into the `master` branch.
* This will trigger github actions to start building a draft release
3. After github actions has finished building. Go to Release page in the repository.
4. Click on the draft release and change the tag target to `master`.
5. Add in release notes.
6. Generate gpg signatures.
7. Click publish release.
## Notes
Artifacts attached in the release shouldn't be deleted! These include the yml files (latest, latest-mac, latest-linux). These are all necessary to get auto updating to work correctly.
### Mac
Mac currently uses 2 formats `dmg` and `zip`.
We need the `zip` format for auto updating to work correctly.
We also need the `dmg` because on MacOS Catalina, there is a system bug where extracting the artifact `zip` using the default _Archive Utility_ will make it so the extracted application is invalid and it will fail to open. A work around for this is to extract the `zip` using an alternate program such as _The Unarchiver_.
Once this bug is fixed we can go back to using the `zip` format by itself.

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Signal update available",
"message": "Session update available",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "There is a new version of Signal available.",
"message": "There is a new version of Session available.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates.",
"message": "Press Restart Session to apply the updates.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Restart Signal",
"message": "Restart Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1468,11 +1468,11 @@
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Натиснете Рестарт на Signal за да валидирате промените.",
"message": "Натиснете Рестарт на Session за да валидирате промените.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Рестарт на Signal",
"message": "Рестарт на Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Disponible una actualització del Signal",
"message": "Disponible una actualització del Session",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Hi ha disponible una versió nova del Signal.",
"message": "Hi ha disponible una versió nova del Session.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Premeu Reinicia el Signal per a aplicar les actualitzacions.",
"message": "Premeu Reinicia el Session per a aplicar les actualitzacions.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Reinicia el Signal",
"message": "Reinicia el Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Dostupná aktualizace Signal",
"message": "Dostupná aktualizace Session",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Je k dispozici nová verze aplikace Signal.",
"message": "Je k dispozici nová verze aplikace Session.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Stiskněte na Restartovat Signal pro aplikování změn",
"message": "Stiskněte na Restartovat Session pro aplikování změn",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Restartovat Signal",
"message": "Restartovat Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Signalopdatering tilgængelig",
"message": "Sessionopdatering tilgængelig",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Der er en ny version af Signal tilgængelig.",
"message": "Der er en ny version af Session tilgængelig.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Genstart Signal for at anvende opdateringerne.",
"message": "Genstart Session for at anvende opdateringerne.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Genstart Signal",
"message": "Genstart Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Aktualisierung für Signal verfügbar",
"message": "Aktualisierung für Session verfügbar",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Eine neue Version von Signal ist verfügbar.",
"message": "Eine neue Version von Session ist verfügbar.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Zum Aktualisieren klicke auf »Signal neu starten«.",
"message": "Zum Aktualisieren klicke auf »Session neu starten«.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Signal neu starten",
"message": "Session neu starten",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,11 +1460,11 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Διαθέσιμη ενημέρωση του Signal",
"message": "Διαθέσιμη ενημέρωση του Session",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Μια νέα έκδοση του Signal είναι διαθέσιμη.",
"message": "Μια νέα έκδοση του Session είναι διαθέσιμη.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
@ -1472,7 +1472,7 @@
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Επανεκκίνηση του Signal",
"message": "Επανεκκίνηση του Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1973,6 +1973,15 @@
"autoUpdateLaterButtonLabel": {
"message": "Later"
},
"autoUpdateDownloadButtonLabel": {
"message": "Download"
},
"autoUpdateDownloadedMessage": {
"message": "The new update has been downloaded."
},
"autoUpdateDownloadInstructions": {
"message": "Would you like to download the update?"
},
"leftTheGroup": {
"message": "$name$ left the group",
"description":

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Ĝisdatiĝo de Signal disponeblas",
"message": "Ĝisdatiĝo de Session disponeblas",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Nova versio de Signal disponeblas.",
"message": "Nova versio de Session disponeblas.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Premu „Restartigi Signal-on“ por ĝisdatigi.",
"message": "Premu „Restartigi Session-on“ por ĝisdatigi.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Restartigi Signal-on",
"message": "Restartigi Session-on",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Actualización de Signal Desktop disponible",
"message": "Actualización de Session Desktop disponible",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Hay una nueva versión de Signal Desktop disponible.",
"message": "Hay una nueva versión de Session Desktop disponible.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Pulsa en 'Reiniciar Signal' para aplicar cambios.",
"message": "Pulsa en 'Reiniciar Session' para aplicar cambios.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Reiniciar Signal",
"message": "Reiniciar Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1324,19 +1324,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Actualización de Signal disponible",
"message": "Actualización de Session disponible",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Hay una nueva versión de Signal disponible.",
"message": "Hay una nueva versión de Session disponible.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates.",
"message": "Press Restart Session to apply the updates.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Restart Signal",
"message": "Restart Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Signali uuendus on saadaval",
"message": "Session uuendus on saadaval",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Signalist on saadaval uus versioon.",
"message": "Session on saadaval uus versioon.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Uuenduste paigaldamiseks vajuta \"Taaskäivita Signal\".",
"message": "Uuenduste paigaldamiseks vajuta \"Taaskäivita Session\".",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Taaskäivita Signal",
"message": "Taaskäivita Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "به‌روزرسانی Signal در دسترس است",
"message": "به‌روزرسانی Session در دسترس است",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "نسخه جدیدی از Signal در دسترس است.",
"message": "نسخه جدیدی از Session در دسترس است.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "برای اعمال آپدیت ها Signal را ری استارت کنید.",
"message": "برای اعمال آپدیت ها Session را ری استارت کنید.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "راه اندازی مجدد Signal",
"message": "راه اندازی مجدد Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Signal päivitys saatavilla",
"message": "Session päivitys saatavilla",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Uusi versio Signalista on saatavilla.",
"message": "Uusi versio Session on saatavilla.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Paina Käynnistä Signal uudelleen asentaaksesi päivitykset.",
"message": "Paina Käynnistä Session uudelleen asentaaksesi päivitykset.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Käynnistä Signal uudelleen",
"message": "Käynnistä Session uudelleen",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Une mise à jour de Signal est proposée",
"message": "Une mise à jour de Session est proposée",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Une nouvelle version de Signal est proposée.",
"message": "Une nouvelle version de Session est proposée.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Appuyez sur « Redémarrer Signal » pour appliquer les mises à jour.",
"message": "Appuyez sur « Redémarrer Session » pour appliquer les mises à jour.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Redémarrer Signal",
"message": "Redémarrer Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "עדכון Signal זמין",
"message": "עדכון Session זמין",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "יש גרסה חדשה של Signal זמינה.",
"message": "יש גרסה חדשה של Session זמינה.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "לחץ על הפעל מחדש את Signal כדי להחיל את העדכונים.",
"message": "לחץ על הפעל מחדש את Session כדי להחיל את העדכונים.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "הפעל מחדש את Signal",
"message": "הפעל מחדש את Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Signal update available",
"message": "Session update available",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "There is a new version of Signal available.",
"message": "There is a new version of Session available.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates.",
"message": "Press Restart Session to apply the updates.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Restart Signal",
"message": "Restart Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Dostupna nadogradnja za Signal",
"message": "Dostupna nadogradnja za Session",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Dostupna je nova inačica Signala.",
"message": "Dostupna je nova inačica Session.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates.",
"message": "Press Restart Session to apply the updates.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Restart Signal",
"message": "Restart Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Signal frissítés elérhető",
"message": "Session frissítés elérhető",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "A Signal új verziója érhető el.",
"message": "A Session új verziója érhető el.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Kattints a Signal újraindítására a frissítések alkalmazásához! ",
"message": "Kattints a Session újraindítására a frissítések alkalmazásához! ",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Signal újraindítása",
"message": "Session újraindítása",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Tersedia Signal versi terbaru",
"message": "Tersedia Session versi terbaru",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Tersedia versi terbaru Signal.",
"message": "Tersedia versi terbaru Session.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Tekan memulai awal Signal untuk mendapatkan versi terbaru.",
"message": "Tekan memulai awal Session untuk mendapatkan versi terbaru.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Mulai ulang Signal",
"message": "Mulai ulang Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Aggiornamento Signal disponibile",
"message": "Aggiornamento Session disponibile",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "È disponibile una nuova versione di Signal.",
"message": "È disponibile una nuova versione di Session.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Premi \"Riavvia Signal\" per applicare gli aggiornamenti.",
"message": "Premi \"Riavvia Session\" per applicare gli aggiornamenti.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Riavvia Signal",
"message": "Riavvia Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Signalのアップデートがあります",
"message": "Sessionのアップデートがあります",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "新しく生まれ変わったSignalがあります",
"message": "新しく生まれ変わったSessionがあります",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "アップデートを適用するにはSignalを再起動してください。",
"message": "アップデートを適用するにはSessionを再起動してください。",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Signalを再起動",
"message": "Sessionを再起動",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "មានបច្ចុប្បន្នភាព Signal",
"message": "មានបច្ចុប្បន្នភាព Session",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "មានSignalជំនាន់ថ្មី",
"message": "មានSessionជំនាន់ថ្មី",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "ចុច បើក Signalឡើងវិញ ដើម្បីដំណើការបច្ចុប្បន្នភាព។",
"message": "ចុច បើក Sessionឡើងវិញ ដើម្បីដំណើការបច្ចុប្បន្នភាព។",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "បើកSignal ឡើងវិញ",
"message": "បើកSession ឡើងវិញ",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Signal update available",
"message": "Session update available",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "There is a new version of Signal available.",
"message": "There is a new version of Session available.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates.",
"message": "Press Restart Session to apply the updates.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Restart Signal",
"message": "Restart Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Signal update available",
"message": "Session update available",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "There is a new version of Signal available.",
"message": "There is a new version of Session available.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates.",
"message": "Press Restart Session to apply the updates.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Restart Signal",
"message": "Restart Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Yra prieinamas Signal atnaujinimas",
"message": "Yra prieinamas Session atnaujinimas",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Yra prieinama nauja Signal versija.",
"message": "Yra prieinama nauja Session versija.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Norėdami pritaikyti atnaujinimus, paspauskite \"Paleisti Signal iš naujo\".",
"message": "Norėdami pritaikyti atnaujinimus, paspauskite \"Paleisti Session iš naujo\".",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Paleisti Signal iš naujo",
"message": "Paleisti Session iš naujo",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Signal update available",
"message": "Session update available",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "There is a new version of Signal available.",
"message": "There is a new version of Session available.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates.",
"message": "Press Restart Session to apply the updates.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Restart Signal",
"message": "Restart Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Signal oppdatering tilgjengelig",
"message": "Session oppdatering tilgjengelig",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "En ny versjon av Signal er tilgjengelig",
"message": "En ny versjon av Session er tilgjengelig",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Trykk Restart Signal for å fullføre oppgraderingen.",
"message": "Trykk Restart Session for å fullføre oppgraderingen.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Start Signal På Nytt",
"message": "Start Session På Nytt",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Update voor Signal beschikbaar",
"message": "Update voor Session beschikbaar",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Er is een nieuwe versie van Signal beschikbaar.",
"message": "Er is een nieuwe versie van Session beschikbaar.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Klik op Signal herstarten om de updates toe te passen.",
"message": "Klik op Session herstarten om de updates toe te passen.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Signal herstarten",
"message": "Session herstarten",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Signal-oppdatering tilgjengeleg",
"message": "Session-oppdatering tilgjengeleg",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Ei ny utgåve av Signal er tilgjengeleg",
"message": "Ei ny utgåve av Session er tilgjengeleg",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Trykk «Start Signal på nytt» for å fullføra oppgraderinga.",
"message": "Trykk «Start Session på nytt» for å fullføra oppgraderinga.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Start Signal på nytt",
"message": "Start Session på nytt",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Signal oppdatering tilgjengelig",
"message": "Session oppdatering tilgjengelig",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "En ny versjon av Signal er tilgjengelig",
"message": "En ny versjon av Session er tilgjengelig",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Trykk Restart Signal for å fullføre oppgraderingen.",
"message": "Trykk Restart Session for å fullføre oppgraderingen.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Start Signal På Nytt",
"message": "Start Session På Nytt",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,11 +1460,11 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Dostępna aktualizacja aplikacji Signal",
"message": "Dostępna aktualizacja aplikacji Session",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Dostępna nowa wersja Signal",
"message": "Dostępna nowa wersja Session",
"description": ""
},
"autoUpdateNewVersionInstructions": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Atualização do Signal disponível",
"message": "Atualização do Session disponível",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Uma nova versão do Signal está disponível.",
"message": "Uma nova versão do Session está disponível.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Por favor, toque em 'reiniciar Signal' para aplicar as atualizações.",
"message": "Por favor, toque em 'reiniciar Session' para aplicar as atualizações.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Reiniciar Signal",
"message": "Reiniciar Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Existe uma actualização disponível para o Signal",
"message": "Existe uma actualização disponível para o Session",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Está disponível uma nova versão do Signal.",
"message": "Está disponível uma nova versão do Session.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Pressione 'Reiniciar o Signal' para aplicar as atualizações.",
"message": "Pressione 'Reiniciar o Session' para aplicar as atualizações.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Reiniciar o Signal",
"message": "Reiniciar o Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Este disponibilă o actualizare de Signal ",
"message": "Este disponibilă o actualizare de Session ",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Este disponibilă o nouă versiune de Signal.",
"message": "Este disponibilă o nouă versiune de Session.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Apasă pe Repornire Signal pentru a aplica actualizările.",
"message": "Apasă pe Repornire Session pentru a aplica actualizările.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Repornește Signal",
"message": "Repornește Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Доступно обновление Signal",
"message": "Доступно обновление Session",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Доступна новая версия Signal",
"message": "Доступна новая версия Session",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Для применения обновлений перезапустите Signal.",
"message": "Для применения обновлений перезапустите Session.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Перезапустите Signal",
"message": "Перезапустите Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Dostupná aktualizácia pre Signal",
"message": "Dostupná aktualizácia pre Session",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Je k dispozícii nová verzia Signal.",
"message": "Je k dispozícii nová verzia Session.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Reštartujte Signal pre dokončenie aktualizácie.",
"message": "Reštartujte Session pre dokončenie aktualizácie.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Reštartovať Signal",
"message": "Reštartovať Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Na voljo je posodobitev aplikacije Signal",
"message": "Na voljo je posodobitev aplikacije Session",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Na voljo je nova različica aplikacije Signal.",
"message": "Na voljo je nova različica aplikacije Session.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Za uveljavitev nadgradenj pritisnite tipko Ponovno zaženi Signal",
"message": "Za uveljavitev nadgradenj pritisnite tipko Ponovno zaženi Session",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Ponovno zaženi Signal",
"message": "Ponovno zaženi Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Ka gati përditësim të Signal-it",
"message": "Ka gati përditësim të Session-it",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Ka të gatshëm një version të ri të Signal-it",
"message": "Ka të gatshëm një version të ri të Session-it",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Shtypni Rinise Signal-in që të zbatohen përditësimet.",
"message": "Shtypni Rinise Session-in që të zbatohen përditësimet.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Riniseni Signal-in",
"message": "Riniseni Session-in",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Нова верзија Signal-а је доступна",
"message": "Нова верзија Session-а је доступна",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "There is a new version of Signal available.",
"message": "There is a new version of Session available.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates.",
"message": "Press Restart Session to apply the updates.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Restart Signal",
"message": "Restart Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Uppdatering för Signal tillgänglig",
"message": "Uppdatering för Session tillgänglig",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Det finns en ny version av Signal tillgänglig.",
"message": "Det finns en ny version av Session tillgänglig.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Vänligen starta om Signal för att uppdatera",
"message": "Vänligen starta om Session för att uppdatera",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Starta om Signal",
"message": "Starta om Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "มีการอัพเดทสำหรับ Signal",
"message": "มีการอัพเดทสำหรับ Session",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "มี Signal รุ่นใหม่แล้ว",
"message": "มี Session รุ่นใหม่แล้ว",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "กด เริ่มต้น Signal ใหม่เพื่อเริ่มใช้การอัพเดต",
"message": "กด เริ่มต้น Session ใหม่เพื่อเริ่มใช้การอัพเดต",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "เริ่มต้น Signal ใหม่",
"message": "เริ่มต้น Session ใหม่",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Signal güncellemesi mevcut",
"message": "Session güncellemesi mevcut",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Signal'ın yeni bir sürümü mevcut.",
"message": "Session'ın yeni bir sürümü mevcut.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Güncellemeleri uygulamak için 'Signal'i Yeniden Başlat'a basınız.",
"message": "Güncellemeleri uygulamak için 'Session'i Yeniden Başlat'a basınız.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Signal'i Yeniden Başlat",
"message": "Session'i Yeniden Başlat",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Доступне оновлення Signal",
"message": "Доступне оновлення Session",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Нова версія Signal доступна.",
"message": "Нова версія Session доступна.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates.",
"message": "Press Restart Session to apply the updates.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Restart Signal",
"message": "Restart Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Signal update available",
"message": "Session update available",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "There is a new version of Signal available.",
"message": "There is a new version of Session available.",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates.",
"message": "Press Restart Session to apply the updates.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Restart Signal",
"message": "Restart Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Signal 有可用更新",
"message": "Session 有可用更新",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "有新版的 Signal 可用。",
"message": "有新版的 Session 可用。",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "点击“重启 Signal”来安装更新。",
"message": "点击“重启 Session",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "重启 Signal",
"message": "重启 Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Signal 可用的更新",
"message": "Session 可用的更新",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "這是新版本的 Signal。",
"message": "這是新版本的 Session",
"description": ""
},
"autoUpdateNewVersionInstructions": {
"message": "點選重啟 Signal 來套用更新。",
"message": "點選重啟 Session 來套用更新。",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "重啟 Signal",
"message": "重啟 Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

@ -23,10 +23,6 @@
"port": "38157"
}
],
"disableAutoUpdate": true,
"updatesUrl": "TODO",
"updatesPublicKey":
"fd7dd3de7149dc0a127909fee7de0f7620ddd0de061b37a2c303e37de802a401",
"updatesEnabled": false,
"openDevTools": false,
"buildExpiration": 0,

@ -1 +1,3 @@
{}
{
"updatesEnabled": true
}

@ -1,5 +1,3 @@
provider: s3
region: us-east-1
bucket: your-test-bucket.signal.org
path: desktop
acl: public-read
owner: <yourGHName>
repo: <yourGHRepoName>
provider: github

@ -65,9 +65,7 @@ const appInstance = config.util.getEnv('NODE_APP_INSTANCE') || 0;
const attachments = require('./app/attachments');
const attachmentChannel = require('./app/attachment_channel');
// TODO: Enable when needed
// const updater = require('./ts/updater/index');
const updater = null;
const updater = require('./ts/updater/index');
const createTrayIcon = require('./app/tray_icon');
const ephemeralConfig = require('./app/ephemeral_config');
@ -410,22 +408,40 @@ ipc.on('show-window', () => {
showWindow();
});
let updatesStarted = false;
ipc.on('ready-for-updates', async () => {
if (updatesStarted || !updater) {
let isReadyForUpdates = false;
async function readyForUpdates() {
if (isReadyForUpdates) {
return;
}
updatesStarted = true;
isReadyForUpdates = true;
// disable for now
/*
// First, install requested sticker pack
const incomingUrl = getIncomingUrl(process.argv);
if (incomingUrl) {
handleSgnlLink(incomingUrl);
}
*/
// Second, start checking for app updates
try {
await updater.start(getMainWindow, locale.messages, logger);
} catch (error) {
logger.error(
const log = logger || console;
log.error(
'Error starting update checks:',
error && error.stack ? error.stack : error
);
}
});
}
ipc.once('ready-for-updates', readyForUpdates);
// Forcefully call readyForUpdates after 10 minutes.
// This ensures we start the updater.
const TEN_MINUTES = 10 * 60 * 1000;
setTimeout(readyForUpdates, TEN_MINUTES);
function openReleaseNotes() {
shell.openExternal(
@ -842,6 +858,9 @@ async function showMainWindow(sqlKey, passwordAttempt = false) {
}
setupMenu();
// Check updates
readyForUpdates();
}
function setupMenu(options) {

@ -2,13 +2,16 @@
"name": "session-messenger-desktop",
"productName": "Session",
"description": "Private messaging from your desktop",
"repository": "https://github.com/loki-project/loki-messenger.git",
"version": "1.0.3",
"license": "GPL-3.0",
"author": {
"name": "Loki Project",
"email": "team@loki.network"
},
"repository": {
"type": "git",
"url": "https://github.com/loki-project/session-desktop.git"
},
"main": "main.js",
"scripts": {
"postinstall": "electron-builder install-app-deps && rimraf node_modules/dtrace-provider",
@ -25,7 +28,6 @@
"build": "electron-builder --config.extraMetadata.environment=$SIGNAL_ENV",
"build-release": "cross-env SIGNAL_ENV=production npm run build -- --config.directories.output=release",
"make:linux:x64:appimage": "electron-builder build --linux appimage --x64",
"sign-release": "node ts/updater/generateSignature.js",
"build-module-protobuf": "pbjs --target static-module --wrap commonjs --out ts/protobuf/compiled.js protos/*.proto && pbts --out ts/protobuf/compiled.d.ts ts/protobuf/compiled.js",
"clean-module-protobuf": "rm -f ts/protobuf/compiled.d.ts ts/protobuf/compiled.js",
"build-protobuf": "yarn build-module-protobuf",
@ -78,8 +80,9 @@
"dompurify": "^2.0.7",
"electron-context-menu": "^0.15.0",
"electron-editor-context-menu": "1.1.1",
"electron-is-dev": "0.3.0",
"electron-is-dev": "^1.1.0",
"electron-localshortcut": "^3.2.1",
"electron-updater": "^4.2.2",
"emoji-datasource": "4.0.0",
"emoji-datasource-apple": "4.0.0",
"emoji-js": "3.4.0",
@ -139,6 +142,7 @@
"@types/classnames": "2.2.3",
"@types/color": "^3.0.0",
"@types/config": "0.0.34",
"@types/electron-is-dev": "^1.1.1",
"@types/filesize": "3.6.0",
"@types/fs-extra": "5.0.5",
"@types/google-libphonenumber": "7.4.14",
@ -208,12 +212,13 @@
"build": {
"appId": "com.loki-project.messenger-desktop",
"afterSign": "build/notarize.js",
"artifactName": "${name}-${os}-${version}.${ext}",
"artifactName": "${name}-${os}-${arch}-${version}.${ext}",
"mac": {
"category": "public.app-category.social-networking",
"icon": "build/icons/mac/icon.icns",
"target": [
"dmg"
"dmg",
"zip"
],
"bundleVersion": "1",
"hardenedRuntime": true,
@ -227,6 +232,7 @@
"win": {
"asarUnpack": "node_modules/spellchecker/vendor/hunspell_dictionaries",
"publisherName": "Loki Project",
"verifyUpdateCodeSignature": false,
"icon": "build/icons/win/icon.ico",
"target": [
"nsis"
@ -318,7 +324,8 @@
"!node_modules/@journeyapps/sqlcipher/deps/*",
"!node_modules/@journeyapps/sqlcipher/build/*",
"!node_modules/@journeyapps/sqlcipher/lib/binding/node-*",
"!build/*.js"
"!build/*.js",
"!dev-app-update.yml"
]
}
}

@ -255,17 +255,17 @@ export class ConversationHeader extends React.Component<Props> {
);
}
public renderSearch() {
return (
<div className="search-icon">
<SessionIconButton
iconType={SessionIconType.Search}
iconSize={SessionIconSize.Large}
iconPadded={true}
onClick={this.highlightMessageSearch}
/>
</div>
);
public renderSearch() {
return (
<div className="search-icon">
<SessionIconButton
iconType={SessionIconType.Search}
iconSize={SessionIconSize.Large}
iconPadded={true}
onClick={this.highlightMessageSearch}
/>
</div>
);
}
public renderOptions(triggerId: string) {
@ -398,12 +398,8 @@ export class ConversationHeader extends React.Component<Props> {
</div>
</div>
{this.renderExpirationLength()}
{!this.props.isRss && (
<>
{this.renderAvatar()}
</>
)}
{!this.props.isRss && <>{this.renderAvatar()}</>}
{!this.props.isRss && this.renderAvatar()}
@ -419,10 +415,10 @@ export class ConversationHeader extends React.Component<Props> {
}
}
public highlightMessageSearch() {
// This is a temporary fix. In future we want to search
// messages in the current conversation
$('.session-search-input input').focus();
public highlightMessageSearch() {
// This is a temporary fix. In future we want to search
// messages in the current conversation
$('.session-search-input input').focus();
}
private renderPublicMenuItems() {

@ -1,77 +0,0 @@
import { assert } from 'chai';
import { getUpdateFileName, getVersion } from '../../updater/common';
describe('updater/signatures', () => {
const windows = `version: 1.23.2
files:
- url: signal-desktop-win-1.23.2.exe
sha512: hhK+cVAb+QOK/Ln0RBcq8Rb1iPcUC0KZeT4NwLB25PMGoPmakY27XE1bXq4QlkASJN1EkYTbKf3oUJtcllziyQ==
size: 92020776
path: signal-desktop-win-1.23.2.exe
sha512: hhK+cVAb+QOK/Ln0RBcq8Rb1iPcUC0KZeT4NwLB25PMGoPmakY27XE1bXq4QlkASJN1EkYTbKf3oUJtcllziyQ==
releaseDate: '2019-03-29T16:58:08.210Z'
`;
const mac = `version: 1.23.2
files:
- url: signal-desktop-mac-1.23.2.zip
sha512: f4pPo3WulTVi9zBWGsJPNIlvPOTCxPibPPDmRFDoXMmFm6lqJpXZQ9DSWMJumfc4BRp4y/NTQLGYI6b4WuJwhg==
size: 105179791
blockMapSize: 111109
path: signal-desktop-mac-1.23.2.zip
sha512: f4pPo3WulTVi9zBWGsJPNIlvPOTCxPibPPDmRFDoXMmFm6lqJpXZQ9DSWMJumfc4BRp4y/NTQLGYI6b4WuJwhg==
releaseDate: '2019-03-29T16:57:16.997Z'
`;
const windowsBeta = `version: 1.23.2-beta.1
files:
- url: signal-desktop-beta-win-1.23.2-beta.1.exe
sha512: ZHM1F3y/Y6ulP5NhbFuh7t2ZCpY4lD9BeBhPV+g2B/0p/66kp0MJDeVxTgjR49OakwpMAafA1d6y2QBail4hSQ==
size: 92028656
path: signal-desktop-beta-win-1.23.2-beta.1.exe
sha512: ZHM1F3y/Y6ulP5NhbFuh7t2ZCpY4lD9BeBhPV+g2B/0p/66kp0MJDeVxTgjR49OakwpMAafA1d6y2QBail4hSQ==
releaseDate: '2019-03-29T01:56:00.544Z'
`;
const macBeta = `version: 1.23.2-beta.1
files:
- url: signal-desktop-beta-mac-1.23.2-beta.1.zip
sha512: h/01N0DD5Jw2Q6M1n4uLGLTCrMFxcn8QOPtLR3HpABsf3w9b2jFtKb56/2cbuJXP8ol8TkTDWKnRV6mnqnLBDw==
size: 105182398
blockMapSize: 110894
path: signal-desktop-beta-mac-1.23.2-beta.1.zip
sha512: h/01N0DD5Jw2Q6M1n4uLGLTCrMFxcn8QOPtLR3HpABsf3w9b2jFtKb56/2cbuJXP8ol8TkTDWKnRV6mnqnLBDw==
releaseDate: '2019-03-29T01:53:23.881Z'
`;
describe('#getVersion', () => {
it('successfully gets version', () => {
const expected = '1.23.2';
assert.strictEqual(getVersion(windows), expected);
assert.strictEqual(getVersion(mac), expected);
const expectedBeta = '1.23.2-beta.1';
assert.strictEqual(getVersion(windowsBeta), expectedBeta);
assert.strictEqual(getVersion(macBeta), expectedBeta);
});
});
describe('#getUpdateFileName', () => {
it('successfully gets version', () => {
assert.strictEqual(
getUpdateFileName(windows),
'signal-desktop-win-1.23.2.exe'
);
assert.strictEqual(
getUpdateFileName(mac),
'signal-desktop-mac-1.23.2.zip'
);
assert.strictEqual(
getUpdateFileName(windowsBeta),
'signal-desktop-beta-win-1.23.2-beta.1.exe'
);
assert.strictEqual(
getUpdateFileName(macBeta),
'signal-desktop-beta-mac-1.23.2-beta.1.zip'
);
});
});
});

@ -1,206 +0,0 @@
import { existsSync } from 'fs';
import { join } from 'path';
import { assert } from 'chai';
import { copy } from 'fs-extra';
import {
_getFileHash,
getSignaturePath,
loadHexFromPath,
verifySignature,
writeHexToPath,
writeSignature,
} from '../../updater/signature';
import { createTempDir, deleteTempDir } from '../../updater/common';
import { keyPair } from '../../updater/curve';
describe('updater/signatures', () => {
it('_getFileHash returns correct hash', async () => {
const filePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
const expected =
'7bc77f27d92d00b4a1d57c480ca86dacc43d57bc318339c92119d1fbf6b557a5';
const hash = await _getFileHash(filePath);
assert.strictEqual(expected, Buffer.from(hash).toString('hex'));
});
it('roundtrips binary file writes', async () => {
let tempDir;
try {
tempDir = await createTempDir();
const path = join(tempDir, 'something.bin');
const { publicKey } = keyPair();
await writeHexToPath(path, publicKey);
const fromDisk = await loadHexFromPath(path);
assert.strictEqual(
Buffer.from(fromDisk).compare(Buffer.from(publicKey)),
0
);
} finally {
if (tempDir) {
await deleteTempDir(tempDir);
}
}
});
it('roundtrips signature', async () => {
let tempDir;
try {
tempDir = await createTempDir();
const version = 'v1.23.2';
const sourcePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
const updatePath = join(tempDir, 'ghost-kitty.mp4');
await copy(sourcePath, updatePath);
const privateKeyPath = join(tempDir, 'private.key');
const { publicKey, privateKey } = keyPair();
await writeHexToPath(privateKeyPath, privateKey);
await writeSignature(updatePath, version, privateKeyPath);
const signaturePath = getSignaturePath(updatePath);
assert.strictEqual(existsSync(signaturePath), true);
const verified = await verifySignature(updatePath, version, publicKey);
assert.strictEqual(verified, true);
} finally {
if (tempDir) {
await deleteTempDir(tempDir);
}
}
});
it('fails signature verification if version changes', async () => {
let tempDir;
try {
tempDir = await createTempDir();
const version = 'v1.23.2';
const brokenVersion = 'v1.23.3';
const sourcePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
const updatePath = join(tempDir, 'ghost-kitty.mp4');
await copy(sourcePath, updatePath);
const privateKeyPath = join(tempDir, 'private.key');
const { publicKey, privateKey } = keyPair();
await writeHexToPath(privateKeyPath, privateKey);
await writeSignature(updatePath, version, privateKeyPath);
const verified = await verifySignature(
updatePath,
brokenVersion,
publicKey
);
assert.strictEqual(verified, false);
} finally {
if (tempDir) {
await deleteTempDir(tempDir);
}
}
});
it('fails signature verification if signature tampered with', async () => {
let tempDir;
try {
tempDir = await createTempDir();
const version = 'v1.23.2';
const sourcePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
const updatePath = join(tempDir, 'ghost-kitty.mp4');
await copy(sourcePath, updatePath);
const privateKeyPath = join(tempDir, 'private.key');
const { publicKey, privateKey } = keyPair();
await writeHexToPath(privateKeyPath, privateKey);
await writeSignature(updatePath, version, privateKeyPath);
const signaturePath = getSignaturePath(updatePath);
const signature = Buffer.from(await loadHexFromPath(signaturePath));
signature[4] += 3;
await writeHexToPath(signaturePath, signature);
const verified = await verifySignature(updatePath, version, publicKey);
assert.strictEqual(verified, false);
} finally {
if (tempDir) {
await deleteTempDir(tempDir);
}
}
});
it('fails signature verification if binary file tampered with', async () => {
let tempDir;
try {
tempDir = await createTempDir();
const version = 'v1.23.2';
const sourcePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
const updatePath = join(tempDir, 'ghost-kitty.mp4');
await copy(sourcePath, updatePath);
const privateKeyPath = join(tempDir, 'private.key');
const { publicKey, privateKey } = keyPair();
await writeHexToPath(privateKeyPath, privateKey);
await writeSignature(updatePath, version, privateKeyPath);
const brokenSourcePath = join(
__dirname,
'../../../fixtures/pixabay-Soap-Bubble-7141.mp4'
);
await copy(brokenSourcePath, updatePath);
const verified = await verifySignature(updatePath, version, publicKey);
assert.strictEqual(verified, false);
} finally {
if (tempDir) {
await deleteTempDir(tempDir);
}
}
});
it('fails signature verification if signed by different key', async () => {
let tempDir;
try {
tempDir = await createTempDir();
const version = 'v1.23.2';
const sourcePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
const updatePath = join(tempDir, 'ghost-kitty.mp4');
await copy(sourcePath, updatePath);
const privateKeyPath = join(tempDir, 'private.key');
const { publicKey } = keyPair();
const { privateKey } = keyPair();
await writeHexToPath(privateKeyPath, privateKey);
await writeSignature(updatePath, version, privateKeyPath);
const verified = await verifySignature(updatePath, version, publicKey);
assert.strictEqual(verified, false);
} finally {
if (tempDir) {
await deleteTempDir(tempDir);
}
}
});
});

@ -1,28 +1,4 @@
import {
createWriteStream,
statSync,
writeFile as writeFileCallback,
} from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
// @ts-ignore
import { createParser } from 'dashdash';
// @ts-ignore
import ProxyAgent from 'proxy-agent';
import { FAILSAFE_SCHEMA, safeLoad } from 'js-yaml';
import { gt } from 'semver';
import { get as getFromConfig } from 'config';
import { get, GotOptions, stream } from 'got';
import { v4 as getGuid } from 'uuid';
import pify from 'pify';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import { app, BrowserWindow, dialog } from 'electron';
// @ts-ignore
import * as packageJson from '../../package.json';
import { getSignatureFileName } from './signature';
import { BrowserWindow, dialog } from 'electron';
export type MessagesType = {
[key: string]: {
@ -42,88 +18,30 @@ export type LoggerType = {
trace: LogFunction;
};
const writeFile = pify(writeFileCallback);
const mkdirpPromise = pify(mkdirp);
const rimrafPromise = pify(rimraf);
const { platform } = process;
export async function checkForUpdates(
logger: LoggerType
): Promise<{
fileName: string;
version: string;
} | null> {
const yaml = await getUpdateYaml();
const version = getVersion(yaml);
if (!version) {
logger.warn('checkForUpdates: no version extracted from downloaded yaml');
return null;
}
if (isVersionNewer(version)) {
logger.info(`checkForUpdates: found newer version ${version}`);
return {
fileName: getUpdateFileName(yaml),
version,
};
}
logger.info(
`checkForUpdates: ${version} is not newer; no new update available`
);
return null;
}
export async function downloadUpdate(
fileName: string,
logger: LoggerType
): Promise<string> {
const baseUrl = getUpdatesBase();
const updateFileUrl = `${baseUrl}/${fileName}`;
const signatureFileName = getSignatureFileName(fileName);
const signatureUrl = `${baseUrl}/${signatureFileName}`;
let tempDir;
try {
tempDir = await createTempDir();
const targetUpdatePath = join(tempDir, fileName);
const targetSignaturePath = join(tempDir, getSignatureFileName(fileName));
logger.info(`downloadUpdate: Downloading ${signatureUrl}`);
const { body } = await get(signatureUrl, getGotOptions());
await writeFile(targetSignaturePath, body);
logger.info(`downloadUpdate: Downloading ${updateFileUrl}`);
const downloadStream = stream(updateFileUrl, getGotOptions());
const writeStream = createWriteStream(targetUpdatePath);
await new Promise((resolve, reject) => {
downloadStream.on('error', error => {
reject(error);
});
downloadStream.on('end', () => {
resolve();
});
writeStream.on('error', error => {
reject(error);
});
export async function showDownloadUpdateDialog(
mainWindow: BrowserWindow,
messages: MessagesType
): Promise<boolean> {
const DOWNLOAD_BUTTON = 0;
const LATER_BUTTON = 1;
const options = {
type: 'info',
buttons: [
messages.autoUpdateDownloadButtonLabel.message,
messages.autoUpdateLaterButtonLabel.message,
],
title: messages.autoUpdateNewVersionTitle.message,
message: messages.autoUpdateNewVersionMessage.message,
detail: messages.autoUpdateDownloadInstructions.message,
defaultId: LATER_BUTTON,
cancelId: DOWNLOAD_BUTTON,
};
downloadStream.pipe(writeStream);
return new Promise(resolve => {
dialog.showMessageBox(mainWindow, options, response => {
resolve(response === DOWNLOAD_BUTTON);
});
return targetUpdatePath;
} catch (error) {
if (tempDir) {
await deleteTempDir(tempDir);
}
throw error;
}
});
}
export async function showUpdateDialog(
@ -139,7 +57,7 @@ export async function showUpdateDialog(
messages.autoUpdateLaterButtonLabel.message,
],
title: messages.autoUpdateNewVersionTitle.message,
message: messages.autoUpdateNewVersionMessage.message,
message: messages.autoUpdateDownloadedMessage.message,
detail: messages.autoUpdateNewVersionInstructions.message,
defaultId: LATER_BUTTON,
cancelId: RESTART_BUTTON,
@ -147,16 +65,10 @@ export async function showUpdateDialog(
return new Promise(resolve => {
dialog.showMessageBox(mainWindow, options, response => {
if (response === RESTART_BUTTON) {
// It's key to delay any install calls here because they don't seem to work inside this
// callback - but only if the message box has a parent window.
// Fixes this: https://github.com/signalapp/Signal-Desktop/issues/1864
resolve(true);
return;
}
resolve(false);
// It's key to delay any install calls here because they don't seem to work inside this
// callback - but only if the message box has a parent window.
// Fixes this: https://github.com/signalapp/Signal-Desktop/issues/1864
resolve(response === RESTART_BUTTON);
});
});
}
@ -179,145 +91,6 @@ export async function showCannotUpdateDialog(
});
}
// Helper functions
export function getUpdateCheckUrl(): string {
return `${getUpdatesBase()}/${getUpdatesFileName()}`;
}
export function getUpdatesBase(): string {
return getFromConfig('updatesUrl');
}
export function getCertificateAuthority(): string {
return getFromConfig('certificateAuthority');
}
export function getProxyUrl(): string | undefined {
return process.env.HTTPS_PROXY || process.env.https_proxy;
}
export function getUpdatesFileName(): string {
const prefix = isBetaChannel() ? 'beta' : 'latest';
if (platform === 'darwin') {
return `${prefix}-mac.yml`;
} else {
return `${prefix}.yml`;
}
}
const hasBeta = /beta/i;
function isBetaChannel(): boolean {
return hasBeta.test(packageJson.version);
}
function isVersionNewer(newVersion: string): boolean {
const { version } = packageJson;
return gt(newVersion, version);
}
export function getVersion(yaml: string): string | undefined {
const info = parseYaml(yaml);
if (info && info.version) {
return info.version;
}
return;
}
export function getUpdateFileName(yaml: string) {
const info = parseYaml(yaml);
if (info && info.path) {
return info.path;
}
return;
}
function parseYaml(yaml: string): any {
return safeLoad(yaml, { schema: FAILSAFE_SCHEMA, json: true });
}
async function getUpdateYaml(): Promise<string> {
const targetUrl = getUpdateCheckUrl();
const { body } = await get(targetUrl, getGotOptions());
if (!body) {
throw new Error('Got unexpected response back from update check');
}
return body.toString('utf8');
}
function getGotOptions(): GotOptions<null> {
const ca = getCertificateAuthority();
const proxyUrl = getProxyUrl();
const agent = proxyUrl ? new ProxyAgent(proxyUrl) : undefined;
return {
agent,
ca,
headers: {
'Cache-Control': 'no-cache',
'User-Agent': 'Session Desktop (+https://getsession.org)',
},
useElectronNet: false,
};
}
function getBaseTempDir() {
// We only use tmpdir() when this code is run outside of an Electron app (as in: tests)
return app ? join(app.getPath('userData'), 'temp') : tmpdir();
}
export async function createTempDir() {
const baseTempDir = getBaseTempDir();
const uniqueName = getGuid();
const targetDir = join(baseTempDir, uniqueName);
await mkdirpPromise(targetDir);
return targetDir;
}
export async function deleteTempDir(targetDir: string) {
const pathInfo = statSync(targetDir);
if (!pathInfo.isDirectory()) {
throw new Error(
`deleteTempDir: Cannot delete path '${targetDir}' because it is not a directory`
);
}
const baseTempDir = getBaseTempDir();
if (!targetDir.startsWith(baseTempDir)) {
throw new Error(
`deleteTempDir: Cannot delete path '${targetDir}' since it is not within base temp dir`
);
}
await rimrafPromise(targetDir);
}
export function getPrintableError(error: Error) {
return error && error.stack ? error.stack : error;
}
export async function deleteBaseTempDir() {
const baseTempDir = getBaseTempDir();
await rimrafPromise(baseTempDir);
}
export function getCliOptions<T>(options: any): T {
const parser = createParser({ options });
const cliOptions = parser.parse(process.argv);
if (cliOptions.help) {
const help = parser.help().trimRight();
// tslint:disable-next-line:no-console
console.log(help);
process.exit(0);
}
return cliOptions;
}

@ -1,45 +0,0 @@
import { getCliOptions, getPrintableError } from './common';
import { keyPair } from './curve';
import { writeHexToPath } from './signature';
/* tslint:disable:no-console */
const OPTIONS = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Print this help and exit.',
},
{
names: ['key', 'k'],
type: 'string',
help: 'Path where public key will go',
default: 'public.key',
},
{
names: ['private', 'p'],
type: 'string',
help: 'Path where private key will go',
default: 'private.key',
},
];
type OptionsType = {
key: string;
private: string;
};
const cliOptions = getCliOptions<OptionsType>(OPTIONS);
go(cliOptions).catch(error => {
console.error('Something went wrong!', getPrintableError(error));
});
async function go(options: OptionsType) {
const { key: publicKeyPath, private: privateKeyPath } = options;
const { publicKey, privateKey } = keyPair();
await Promise.all([
writeHexToPath(publicKeyPath, publicKey),
writeHexToPath(privateKeyPath, privateKey),
]);
}

@ -1,85 +0,0 @@
import { join, resolve } from 'path';
import { readdir as readdirCallback } from 'fs';
import pify from 'pify';
import { getCliOptions, getPrintableError } from './common';
import { writeSignature } from './signature';
// @ts-ignore
import * as packageJson from '../../package.json';
const readdir = pify(readdirCallback);
/* tslint:disable:no-console */
const OPTIONS = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Print this help and exit.',
},
{
names: ['private', 'p'],
type: 'string',
help: 'Path to private key file (default: ./private.key)',
default: 'private.key',
},
{
names: ['update', 'u'],
type: 'string',
help: 'Path to the update package (default: the .exe or .zip in ./release)',
},
{
names: ['version', 'v'],
type: 'string',
help: `Version number of this package (default: ${packageJson.version})`,
default: packageJson.version,
},
];
type OptionsType = {
private: string;
update: string;
version: string;
};
const cliOptions = getCliOptions<OptionsType>(OPTIONS);
go(cliOptions).catch(error => {
console.error('Something went wrong!', getPrintableError(error));
});
async function go(options: OptionsType) {
const { private: privateKeyPath, version } = options;
let { update: updatePath } = options;
if (!updatePath) {
updatePath = await findUpdatePath();
}
console.log('Signing with...');
console.log(` version: ${version}`);
console.log(` update file: ${updatePath}`);
console.log(` private key file: ${privateKeyPath}`);
await writeSignature(updatePath, version, privateKeyPath);
}
const IS_EXE = /\.exe$/;
const IS_ZIP = /\.zip$/;
async function findUpdatePath(): Promise<string> {
const releaseDir = resolve('release');
const files: Array<string> = await readdir(releaseDir);
const max = files.length;
for (let i = 0; i < max; i += 1) {
const file = files[i];
const fullPath = join(releaseDir, file);
if (IS_EXE.test(file) || IS_ZIP.test(file)) {
return fullPath;
}
}
throw new Error("No suitable file found in 'release' folder!");
}

@ -1,14 +1,7 @@
import { get as getFromConfig } from 'config';
import { BrowserWindow } from 'electron';
import { start as startMacOS } from './macos';
import { start as startWindows } from './windows';
import {
deleteBaseTempDir,
getPrintableError,
LoggerType,
MessagesType,
} from './common';
import { start as startUpdater } from './updater';
import { LoggerType, MessagesType } from './common';
let initialized = false;
@ -17,8 +10,6 @@ export async function start(
messages?: MessagesType,
logger?: LoggerType
) {
const { platform } = process;
if (initialized) {
throw new Error('updater/start: Updates have already been initialized!');
}
@ -32,6 +23,13 @@ export async function start(
}
if (autoUpdateDisabled()) {
/*
If you really want to enable auto-updating in dev mode
You need to create a dev-app-update.yml file.
A sample can be found in dev-app-update.yml.sample.
After that you can change `updatesEnabled` to `true` in the default config.
*/
logger.info(
'updater/start: Updates disabled - not starting new version checks'
);
@ -39,28 +37,11 @@ export async function start(
return;
}
try {
await deleteBaseTempDir();
} catch (error) {
logger.error(
'updater/start: Error deleting temp dir:',
getPrintableError(error)
);
}
if (platform === 'win32') {
await startWindows(getMainWindow, messages, logger);
} else if (platform === 'darwin') {
await startMacOS(getMainWindow, messages, logger);
} else {
throw new Error('updater/start: Unsupported platform');
}
await startUpdater(getMainWindow, messages, logger);
}
function autoUpdateDisabled() {
return (
process.platform === 'linux' ||
process.mas ||
!getFromConfig('updatesEnabled')
process.mas || !getFromConfig('updatesEnabled') // From Electron: Mac App Store build
);
}

@ -1,324 +0,0 @@
import { createReadStream, statSync } from 'fs';
import { createServer, IncomingMessage, Server, ServerResponse } from 'http';
import { AddressInfo } from 'net';
import { dirname } from 'path';
import { v4 as getGuid } from 'uuid';
import { app, autoUpdater, BrowserWindow, dialog } from 'electron';
import { get as getFromConfig } from 'config';
import { gt } from 'semver';
import {
checkForUpdates,
deleteTempDir,
downloadUpdate,
getPrintableError,
LoggerType,
MessagesType,
showCannotUpdateDialog,
showUpdateDialog,
} from './common';
import { hexToBinary, verifySignature } from './signature';
import { markShouldQuit } from '../../app/window_state';
let isChecking = false;
const SECOND = 1000;
const MINUTE = SECOND * 60;
const INTERVAL = MINUTE * 30;
export async function start(
getMainWindow: () => BrowserWindow,
messages: MessagesType,
logger: LoggerType
) {
logger.info('macos/start: starting checks...');
loggerForQuitHandler = logger;
app.once('quit', quitHandler);
setInterval(async () => {
try {
await checkDownloadAndInstall(getMainWindow, messages, logger);
} catch (error) {
logger.error('macos/start: error:', getPrintableError(error));
}
}, INTERVAL);
await checkDownloadAndInstall(getMainWindow, messages, logger);
}
let fileName: string;
let version: string;
let updateFilePath: string;
let loggerForQuitHandler: LoggerType;
async function checkDownloadAndInstall(
getMainWindow: () => BrowserWindow,
messages: MessagesType,
logger: LoggerType
) {
if (isChecking) {
return;
}
logger.info('checkDownloadAndInstall: checking for update...');
try {
isChecking = true;
const result = await checkForUpdates(logger);
if (!result) {
return;
}
const { fileName: newFileName, version: newVersion } = result;
if (fileName !== newFileName || !version || gt(newVersion, version)) {
deleteCache(updateFilePath, logger);
fileName = newFileName;
version = newVersion;
updateFilePath = await downloadUpdate(fileName, logger);
}
const publicKey = hexToBinary(getFromConfig('updatesPublicKey'));
const verified = verifySignature(updateFilePath, version, publicKey);
if (!verified) {
// Note: We don't delete the cache here, because we don't want to continually
// re-download the broken release. We will download it only once per launch.
throw new Error(
`checkDownloadAndInstall: Downloaded update did not pass signature verification (version: '${version}'; fileName: '${fileName}')`
);
}
try {
await handToAutoUpdate(updateFilePath, logger);
} catch (error) {
const readOnly = 'Cannot update while running on a read-only volume';
const message: string = error.message || '';
if (message.includes(readOnly)) {
logger.info('checkDownloadAndInstall: showing read-only dialog...');
await showReadOnlyDialog(getMainWindow(), messages);
} else {
logger.info(
'checkDownloadAndInstall: showing general update failure dialog...'
);
await showCannotUpdateDialog(getMainWindow(), messages);
}
throw error;
}
// At this point, closing the app will cause the update to be installed automatically
// because Squirrel has cached the update file and will do the right thing.
logger.info('checkDownloadAndInstall: showing update dialog...');
const shouldUpdate = await showUpdateDialog(getMainWindow(), messages);
if (!shouldUpdate) {
return;
}
logger.info('checkDownloadAndInstall: calling quitAndInstall...');
markShouldQuit();
autoUpdater.quitAndInstall();
} catch (error) {
logger.error('checkDownloadAndInstall: error', getPrintableError(error));
} finally {
isChecking = false;
}
}
function quitHandler() {
deleteCache(updateFilePath, loggerForQuitHandler);
}
// Helpers
function deleteCache(filePath: string | null, logger: LoggerType) {
if (filePath) {
const tempDir = dirname(filePath);
deleteTempDir(tempDir).catch(error => {
logger.error(
'quitHandler: error deleting temporary directory:',
getPrintableError(error)
);
});
}
}
async function handToAutoUpdate(
filePath: string,
logger: LoggerType
): Promise<void> {
return new Promise((resolve, reject) => {
const updateFileUrl = generateFileUrl();
const server = createServer();
let serverUrl: string;
server.on('error', (error: Error) => {
logger.error(
'handToAutoUpdate: server had error',
getPrintableError(error)
);
shutdown(server, logger);
reject(error);
});
server.on(
'request',
(request: IncomingMessage, response: ServerResponse) => {
const { url } = request;
if (url === '/') {
const absoluteUrl = `${serverUrl}${updateFileUrl}`;
writeJSONResponse(absoluteUrl, response);
return;
}
if (!url || !url.startsWith(updateFileUrl)) {
write404(url, response, logger);
return;
}
pipeUpdateToSquirrel(filePath, server, response, logger, reject);
}
);
server.listen(0, '127.0.0.1', () => {
serverUrl = getServerUrl(server);
autoUpdater.on('error', (error: Error) => {
logger.error('autoUpdater: error', getPrintableError(error));
reject(error);
});
autoUpdater.on('update-downloaded', () => {
logger.info('autoUpdater: update-downloaded event fired');
shutdown(server, logger);
resolve();
});
autoUpdater.setFeedURL({
url: serverUrl,
headers: { 'Cache-Control': 'no-cache' },
});
autoUpdater.checkForUpdates();
});
});
}
function pipeUpdateToSquirrel(
filePath: string,
server: Server,
response: ServerResponse,
logger: LoggerType,
reject: (error: Error) => void
) {
const updateFileSize = getFileSize(filePath);
const readStream = createReadStream(filePath);
response.on('error', (error: Error) => {
logger.error(
'pipeUpdateToSquirrel: update file download request had an error',
getPrintableError(error)
);
shutdown(server, logger);
reject(error);
});
readStream.on('error', (error: Error) => {
logger.error(
'pipeUpdateToSquirrel: read stream error response:',
getPrintableError(error)
);
shutdown(server, logger, response);
reject(error);
});
response.writeHead(200, {
'Content-Type': 'application/zip',
'Content-Length': updateFileSize,
});
readStream.pipe(response);
}
function writeJSONResponse(url: string, response: ServerResponse) {
const data = Buffer.from(
JSON.stringify({
url,
})
);
response.writeHead(200, {
'Content-Type': 'application/json',
'Content-Length': data.byteLength,
});
response.end(data);
}
function write404(
url: string | undefined,
response: ServerResponse,
logger: LoggerType
) {
logger.error(`write404: Squirrel requested unexpected url '${url}'`);
response.writeHead(404);
response.end();
}
function getServerUrl(server: Server) {
const address = server.address() as AddressInfo;
// tslint:disable-next-line:no-http-string
return `http://127.0.0.1:${address.port}`;
}
function generateFileUrl(): string {
return `/${getGuid()}.zip`;
}
function getFileSize(targetPath: string): number {
const { size } = statSync(targetPath);
return size;
}
function shutdown(
server: Server,
logger: LoggerType,
response?: ServerResponse
) {
try {
if (server) {
server.close();
}
} catch (error) {
logger.error('shutdown: Error closing server', getPrintableError(error));
}
try {
if (response) {
response.end();
}
} catch (endError) {
logger.error(
"shutdown: couldn't end response",
getPrintableError(endError)
);
}
}
export async function showReadOnlyDialog(
mainWindow: BrowserWindow,
messages: MessagesType
): Promise<void> {
const options = {
type: 'warning',
buttons: [messages.ok.message],
title: messages.cannotUpdate.message,
message: messages.readOnlyVolume.message,
};
return new Promise(resolve => {
dialog.showMessageBox(mainWindow, options, () => {
resolve();
});
});
}

@ -1,112 +0,0 @@
import { createHash } from 'crypto';
import {
createReadStream,
readFile as readFileCallback,
writeFile as writeFileCallback,
} from 'fs';
import { basename, dirname, join, resolve as resolvePath } from 'path';
import pify from 'pify';
import { BinaryType, sign, verify } from './curve';
const readFile = pify(readFileCallback);
const writeFile = pify(writeFileCallback);
export async function generateSignature(
updatePackagePath: string,
version: string,
privateKeyPath: string
) {
const privateKey = await loadHexFromPath(privateKeyPath);
const message = await generateMessage(updatePackagePath, version);
return sign(privateKey, message);
}
export async function verifySignature(
updatePackagePath: string,
version: string,
publicKey: BinaryType
): Promise<boolean> {
const signaturePath = getSignaturePath(updatePackagePath);
const signature = await loadHexFromPath(signaturePath);
const message = await generateMessage(updatePackagePath, version);
return verify(publicKey, message, signature);
}
// Helper methods
async function generateMessage(
updatePackagePath: string,
version: string
): Promise<BinaryType> {
const hash = await _getFileHash(updatePackagePath);
const messageString = `${Buffer.from(hash).toString('hex')}-${version}`;
return Buffer.from(messageString);
}
export async function writeSignature(
updatePackagePath: string,
version: string,
privateKeyPath: string
) {
const signaturePath = getSignaturePath(updatePackagePath);
const signature = await generateSignature(
updatePackagePath,
version,
privateKeyPath
);
await writeHexToPath(signaturePath, signature);
}
export async function _getFileHash(
updatePackagePath: string
): Promise<BinaryType> {
const hash = createHash('sha256');
const stream = createReadStream(updatePackagePath);
return new Promise((resolve, reject) => {
stream.on('data', data => {
hash.update(data);
});
stream.on('close', () => {
resolve(hash.digest());
});
stream.on('error', error => {
reject(error);
});
});
}
export function getSignatureFileName(fileName: string) {
return `${fileName}.sig`;
}
export function getSignaturePath(updatePackagePath: string): string {
const updateFullPath = resolvePath(updatePackagePath);
const updateDir = dirname(updateFullPath);
const updateFileName = basename(updateFullPath);
return join(updateDir, getSignatureFileName(updateFileName));
}
export function hexToBinary(target: string): BinaryType {
return Buffer.from(target, 'hex');
}
export function binaryToHex(data: BinaryType): string {
return Buffer.from(data).toString('hex');
}
export async function loadHexFromPath(target: string): Promise<BinaryType> {
const hexString = await readFile(target, 'utf8');
return hexToBinary(hexString);
}
export async function writeHexToPath(target: string, data: BinaryType) {
await writeFile(target, binaryToHex(data));
}

@ -0,0 +1,152 @@
import * as path from 'path';
import * as fs from 'fs-extra';
import { autoUpdater, UpdateInfo } from 'electron-updater';
import { app, BrowserWindow } from 'electron';
import { markShouldQuit } from '../../app/window_state';
import {
getPrintableError,
LoggerType,
MessagesType,
showCannotUpdateDialog,
showDownloadUpdateDialog,
showUpdateDialog,
} from './common';
import { gt as isVersionGreaterThan, parse as parseVersion } from 'semver';
let isUpdating = false;
let downloadIgnored = false;
const SECOND = 1000;
const MINUTE = SECOND * 60;
const INTERVAL = MINUTE * 30;
export async function start(
getMainWindow: () => BrowserWindow,
messages: MessagesType,
logger: LoggerType
) {
logger.info('auto-update: starting checks...');
autoUpdater.logger = logger;
autoUpdater.autoDownload = false;
setInterval(async () => {
try {
await checkForUpdates(getMainWindow, messages, logger);
} catch (error) {
logger.error('auto-update: error:', getPrintableError(error));
}
}, INTERVAL);
await checkForUpdates(getMainWindow, messages, logger);
}
async function checkForUpdates(
getMainWindow: () => BrowserWindow,
messages: MessagesType,
logger: LoggerType
) {
if (isUpdating || downloadIgnored) {
return;
}
const canUpdate = await canAutoUpdate();
if (!canUpdate) {
return;
}
logger.info('auto-update: checking for update...');
isUpdating = true;
try {
// Get the update using electron-updater
const result = await autoUpdater.checkForUpdates();
if (!result.updateInfo) {
logger.info('auto-update: no update info received');
return;
}
try {
const hasUpdate = isUpdateAvailable(result.updateInfo);
if (!hasUpdate) {
logger.info('auto-update: no update available');
return;
}
logger.info('auto-update: showing download dialog...');
const shouldDownload = await showDownloadUpdateDialog(
getMainWindow(),
messages
);
if (!shouldDownload) {
downloadIgnored = true;
return;
}
await autoUpdater.downloadUpdate();
} catch (error) {
await showCannotUpdateDialog(getMainWindow(), messages);
throw error;
}
// Update downloaded successfully, we should ask the user to update
logger.info('auto-update: showing update dialog...');
const shouldUpdate = await showUpdateDialog(getMainWindow(), messages);
if (!shouldUpdate) {
return;
}
logger.info('auto-update: calling quitAndInstall...');
markShouldQuit();
autoUpdater.quitAndInstall();
} finally {
isUpdating = false;
}
}
function isUpdateAvailable(updateInfo: UpdateInfo): boolean {
const latestVersion = parseVersion(updateInfo.version);
if (!latestVersion) {
return false;
}
// We need to convert this to string because typescript won't let us use types across submodules ....
const currentVersion = autoUpdater.currentVersion.toString();
return isVersionGreaterThan(latestVersion, currentVersion);
}
/*
Check if we have the required files to auto update.
These files won't exist inside certain formats such as a linux deb file.
*/
async function canAutoUpdate(): Promise<boolean> {
const isPackaged = app.isPackaged;
// On a production app, we need to use resources path to check for the file
if (isPackaged && !process.resourcesPath) {
return false;
}
// Taken from: https://github.com/electron-userland/electron-builder/blob/d4feb6d3c8b008f8b455c761d654c8088f90d8fa/packages/electron-updater/src/ElectronAppAdapter.ts#L25
const updateFile = isPackaged ? 'app-update.yml' : 'dev-app-update.yml';
const basePath =
isPackaged && process.resourcesPath
? process.resourcesPath
: app.getAppPath();
const appUpdateConfigPath = path.join(basePath, updateFile);
return new Promise(resolve => {
try {
// tslint:disable-next-line: non-literal-fs-path
const exists = fs.existsSync(appUpdateConfigPath);
resolve(exists);
} catch (e) {
resolve(false);
}
});
}

@ -1,231 +0,0 @@
import { dirname, join } from 'path';
import { spawn as spawnEmitter, SpawnOptions } from 'child_process';
import { readdir as readdirCallback, unlink as unlinkCallback } from 'fs';
import { app, BrowserWindow } from 'electron';
import { get as getFromConfig } from 'config';
import { gt } from 'semver';
import pify from 'pify';
import {
checkForUpdates,
deleteTempDir,
downloadUpdate,
getPrintableError,
LoggerType,
MessagesType,
showCannotUpdateDialog,
showUpdateDialog,
} from './common';
import { hexToBinary, verifySignature } from './signature';
import { markShouldQuit } from '../../app/window_state';
const readdir = pify(readdirCallback);
const unlink = pify(unlinkCallback);
let isChecking = false;
const SECOND = 1000;
const MINUTE = SECOND * 60;
const INTERVAL = MINUTE * 30;
export async function start(
getMainWindow: () => BrowserWindow,
messages: MessagesType,
logger: LoggerType
) {
logger.info('windows/start: starting checks...');
loggerForQuitHandler = logger;
app.once('quit', quitHandler);
setInterval(async () => {
try {
await checkDownloadAndInstall(getMainWindow, messages, logger);
} catch (error) {
logger.error('windows/start: error:', getPrintableError(error));
}
}, INTERVAL);
await deletePreviousInstallers(logger);
await checkDownloadAndInstall(getMainWindow, messages, logger);
}
let fileName: string;
let version: string;
let updateFilePath: string;
let installing: boolean;
let loggerForQuitHandler: LoggerType;
async function checkDownloadAndInstall(
getMainWindow: () => BrowserWindow,
messages: MessagesType,
logger: LoggerType
) {
if (isChecking) {
return;
}
try {
isChecking = true;
logger.info('checkDownloadAndInstall: checking for update...');
const result = await checkForUpdates(logger);
if (!result) {
return;
}
const { fileName: newFileName, version: newVersion } = result;
if (fileName !== newFileName || !version || gt(newVersion, version)) {
deleteCache(updateFilePath, logger);
fileName = newFileName;
version = newVersion;
updateFilePath = await downloadUpdate(fileName, logger);
}
const publicKey = hexToBinary(getFromConfig('updatesPublicKey'));
const verified = verifySignature(updateFilePath, version, publicKey);
if (!verified) {
// Note: We don't delete the cache here, because we don't want to continually
// re-download the broken release. We will download it only once per launch.
throw new Error(
`Downloaded update did not pass signature verification (version: '${version}'; fileName: '${fileName}')`
);
}
logger.info('checkDownloadAndInstall: showing dialog...');
const shouldUpdate = await showUpdateDialog(getMainWindow(), messages);
if (!shouldUpdate) {
return;
}
try {
await verifyAndInstall(updateFilePath, version, logger);
installing = true;
} catch (error) {
logger.info(
'checkDownloadAndInstall: showing general update failure dialog...'
);
await showCannotUpdateDialog(getMainWindow(), messages);
throw error;
}
markShouldQuit();
app.quit();
} catch (error) {
logger.error('checkDownloadAndInstall: error', getPrintableError(error));
} finally {
isChecking = false;
}
}
function quitHandler() {
if (updateFilePath && !installing) {
verifyAndInstall(updateFilePath, version, loggerForQuitHandler).catch(
error => {
loggerForQuitHandler.error(
'quitHandler: error installing:',
getPrintableError(error)
);
}
);
}
}
// Helpers
// This is fixed by out new install mechanisms...
// https://github.com/signalapp/Signal-Desktop/issues/2369
// ...but we should also clean up those old installers.
const IS_EXE = /\.exe$/i;
async function deletePreviousInstallers(logger: LoggerType) {
const userDataPath = app.getPath('userData');
const files: Array<string> = await readdir(userDataPath);
await Promise.all(
files.map(async file => {
const isExe = IS_EXE.test(file);
if (!isExe) {
return;
}
const fullPath = join(userDataPath, file);
try {
await unlink(fullPath);
} catch (error) {
logger.error(`deletePreviousInstallers: couldn't delete file ${file}`);
}
})
);
}
async function verifyAndInstall(
filePath: string,
newVersion: string,
logger: LoggerType
) {
const publicKey = hexToBinary(getFromConfig('updatesPublicKey'));
const verified = verifySignature(updateFilePath, newVersion, publicKey);
if (!verified) {
throw new Error(
`Downloaded update did not pass signature verification (version: '${newVersion}'; fileName: '${fileName}')`
);
}
await install(filePath, logger);
}
async function install(filePath: string, logger: LoggerType): Promise<void> {
logger.info('windows/install: installing package...');
const args = ['--updated'];
const options = {
detached: true,
stdio: 'ignore' as 'ignore', // TypeScript considers this a plain string without help
};
try {
await spawn(filePath, args, options);
} catch (error) {
if (error.code === 'UNKNOWN' || error.code === 'EACCES') {
logger.warn(
'windows/install: Error running installer; Trying again with elevate.exe'
);
await spawn(getElevatePath(), [filePath, ...args], options);
return;
}
throw error;
}
}
function deleteCache(filePath: string | null, logger: LoggerType) {
if (filePath) {
const tempDir = dirname(filePath);
deleteTempDir(tempDir).catch(error => {
logger.error(
'deleteCache: error deleting temporary directory',
getPrintableError(error)
);
});
}
}
function getElevatePath() {
const installPath = app.getAppPath();
return join(installPath, 'resources', 'elevate.exe');
}
async function spawn(
exe: string,
args: Array<string>,
options: SpawnOptions
): Promise<void> {
return new Promise((resolve, reject) => {
const emitter = spawnEmitter(exe, args, options);
emitter.on('error', reject);
emitter.unref();
// tslint:disable-next-line no-string-based-set-timeout
setTimeout(resolve, 200);
});
}

@ -191,6 +191,13 @@
dependencies:
"@types/trusted-types" "*"
"@types/electron-is-dev@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@types/electron-is-dev/-/electron-is-dev-1.1.1.tgz#b48cb249b4615915b16477891160414b57b9a8c5"
integrity sha512-axJ7z6N/FfXHf0Q6MO75Sl7gXCqAeIJMxxYd8n80FNmGev8GPHMcva31zQQX+i4B7aBUzdyVY1UfQeFxph3xVQ==
dependencies:
electron-is-dev "*"
"@types/events@*":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
@ -392,6 +399,13 @@
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45"
integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==
"@types/semver@^7.1.0":
version "7.1.0"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.1.0.tgz#c8c630d4c18cd326beff77404887596f96408408"
integrity sha512-pOKLaubrAEMUItGNpgwl0HMFPrSAFic8oSVIvfu1UwcgGNmNyK9gyhBHKmBnUTwwVvpZfkzUC0GaMgnL6P86uA==
dependencies:
"@types/node" "*"
"@types/sinon@4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-4.3.1.tgz#32458f9b166cd44c23844eee4937814276f35199"
@ -3039,12 +3053,7 @@ electron-is-accelerator@^0.1.0:
resolved "https://registry.yarnpkg.com/electron-is-accelerator/-/electron-is-accelerator-0.1.2.tgz#509e510c26a56b55e17f863a4b04e111846ab27b"
integrity sha1-UJ5RDCala1Xhf4Y6SwThEYRqsns=
electron-is-dev@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-0.3.0.tgz#14e6fda5c68e9e4ecbeff9ccf037cbd7c05c5afe"
integrity sha1-FOb9pcaOnk7L7/nM8DfL18BcWv4=
electron-is-dev@^1.0.1:
electron-is-dev@*, electron-is-dev@^1.0.1, electron-is-dev@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-1.1.0.tgz#b15a2a600bdc48a51a857d460e05f15b19a2522c"
integrity sha512-Z1qA/1oHNowGtSBIcWk0pcLEqYT/j+13xUw/MYOrBUOL4X7VN0i0KCTf5SqyvMPmW5pSPKbo28wkxMxzZ20YnQ==
@ -3099,6 +3108,20 @@ electron-to-chromium@^1.2.7:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.334.tgz#0588359f4ac5c4185ebacdf5fc7e1937e2c99872"
integrity sha512-RcjJhpsVaX0X6ntu/WSBlW9HE9pnCgXS9B8mTUObl1aDxaiOa0Lu+NMveIS5IDC+VELzhM32rFJDCC+AApVwcA==
electron-updater@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-4.2.2.tgz#57e106bffad16f71b1ffa3968a52a1b71c8147e6"
integrity sha512-e/OZhr5tLW0GcgmpR5wD0ImxgKMa8pPoNWRcwRyMzTL9pGej7+ORp0t9DtI5ZBHUbObIoEbrk+6EDGUGtJf+aA==
dependencies:
"@types/semver" "^7.1.0"
builder-util-runtime "8.6.0"
fs-extra "^8.1.0"
js-yaml "^3.13.1"
lazy-val "^1.0.4"
lodash.isequal "^4.5.0"
pako "^1.0.11"
semver "^7.1.3"
electron@4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/electron/-/electron-4.1.2.tgz#dc8be0f219c73d60a97675d6d3c5b040c4f50513"
@ -6080,6 +6103,11 @@ lodash.isempty@^4.1.2:
resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e"
integrity sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=
lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
lodash.isfunction@^3.0.8:
version "3.0.9"
resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051"
@ -7334,6 +7362,11 @@ package-json@^6.3.0:
registry-url "^5.0.0"
semver "^6.2.0"
pako@^1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
pako@~1.0.5:
version "1.0.10"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732"
@ -9292,6 +9325,11 @@ semver@^7.1.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.2.tgz#847bae5bce68c5d08889824f02667199b70e3d87"
integrity sha512-BJs9T/H8sEVHbeigqzIEo57Iu/3DG6c4QoqTfbQB3BPA4zgzAomh/Fk9E7QtjWQ8mx2dgA9YCfSF4y9k9bHNpQ==
semver@^7.1.3:
version "7.1.3"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.3.tgz#e4345ce73071c53f336445cfc19efb1c311df2a6"
integrity sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA==
semver@~5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"

Loading…
Cancel
Save