diff --git a/app/build.xml b/app/build.xml index a2926990222..b268b777842 100644 --- a/app/build.xml +++ b/app/build.xml @@ -101,6 +101,7 @@ + diff --git a/app/src/cc/arduino/view/SplashScreenHelper.java b/app/src/cc/arduino/view/SplashScreenHelper.java index 108c1c8b2f6..a67c3f4d48f 100644 --- a/app/src/cc/arduino/view/SplashScreenHelper.java +++ b/app/src/cc/arduino/view/SplashScreenHelper.java @@ -31,11 +31,18 @@ package cc.arduino.view; -import java.awt.*; +import java.awt.Color; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.SplashScreen; +import java.awt.Toolkit; import java.awt.geom.Rectangle2D; +import java.io.File; +import java.io.IOException; import java.util.Map; import processing.app.Theme; +import processing.app.UpdateCheck; public class SplashScreenHelper { @@ -54,6 +61,10 @@ public SplashScreenHelper(SplashScreen splash) { if (splash != null) { Toolkit tk = Toolkit.getDefaultToolkit(); desktopHints = (Map) tk.getDesktopProperty("awt.font.desktophints"); + File image = UpdateCheck.getUpdatedSplashImageFile(); + if (image != null) { + splashImage(image); + } } else { desktopHints = null; } @@ -120,4 +131,12 @@ private void printText(String str) { System.err.println(str); } + public void splashImage(File f) { + try { + splash.setImageURL(f.toURI().toURL()); + } catch (NullPointerException | IllegalStateException | IOException e) { + e.printStackTrace(); + } + } + } diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index 7af728fdd49..336487893c1 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -296,7 +296,7 @@ public Base(String[] args) throws Exception { pdeKeywords = new PdeKeywords(); pdeKeywords.reload(); - final GPGDetachedSignatureVerifier gpgDetachedSignatureVerifier = new GPGDetachedSignatureVerifier(); + final SignatureVerifier gpgDetachedSignatureVerifier = new SignatureVerifier(); contributionInstaller = new ContributionInstaller(BaseNoGui.getPlatform(), gpgDetachedSignatureVerifier); libraryInstaller = new LibraryInstaller(BaseNoGui.getPlatform(), gpgDetachedSignatureVerifier); @@ -1886,8 +1886,16 @@ static public String[] headerListFromIncludePath(File path) throws IOException { */ @SuppressWarnings("serial") public void handleAbout() { - final Image image = Theme.getLibImage("about", activeEditor, - Theme.scale(475), Theme.scale(300)); + Image image; + File f = UpdateCheck.getUpdatedSplashImageFile(); + if (f != null) { + Toolkit tk = Toolkit.getDefaultToolkit(); + Image unscaled = tk.getImage(f.getAbsolutePath()); + image = Theme.scale(unscaled, activeEditor); + } else { + image = Theme.getLibImage("about", activeEditor, // + Theme.scale(475), Theme.scale(300)); + } final Window window = new Window(activeEditor) { public void paint(Graphics graphics) { Graphics2D g = Theme.setupGraphics2D(graphics); diff --git a/app/src/processing/app/Theme.java b/app/src/processing/app/Theme.java index d38875b3597..24a7c2db934 100644 --- a/app/src/processing/app/Theme.java +++ b/app/src/processing/app/Theme.java @@ -575,23 +575,42 @@ static public Image getLibImage(String filename, Component who, int width, image = tk.getImage(imageFile.getUrl()); } + image = rescaleImage(image, who, width, height); + + return image; + } + + public static Image rescaleImage(Image image, Component who, int width, int height) { MediaTracker tracker = new MediaTracker(who); try { tracker.addImage(image, 0); tracker.waitForAll(); } catch (InterruptedException e) { } + if (image.getWidth(null) == width && image.getHeight(null) == height) { + return image; + } - if (image.getWidth(null) != width || image.getHeight(null) != height) { - image = image.getScaledInstance(width, height, Image.SCALE_SMOOTH); - try { - tracker.addImage(image, 1); - tracker.waitForAll(); - } catch (InterruptedException e) { - } + Image rescaled = image.getScaledInstance(width, height, Image.SCALE_SMOOTH); + try { + tracker.addImage(rescaled, 1); + tracker.waitForAll(); + } catch (InterruptedException e) { } + return rescaled; + } - return image; + public static Image scale(Image image, Component who) { + MediaTracker tracker = new MediaTracker(who); + try { + tracker.addImage(image, 0); + tracker.waitForAll(); + } catch (InterruptedException e) { + } + + int w = image.getWidth(null); + int h = image.getHeight(null); + return rescaleImage(image, who, scale(w), scale(h)); } /** diff --git a/app/src/processing/app/UpdateCheck.java b/app/src/processing/app/UpdateCheck.java index cdca1b71783..ec303d424ba 100644 --- a/app/src/processing/app/UpdateCheck.java +++ b/app/src/processing/app/UpdateCheck.java @@ -22,18 +22,29 @@ package processing.app; -import org.apache.commons.compress.utils.IOUtils; -import processing.app.legacy.PApplet; +import static processing.app.I18n.tr; -import javax.swing.*; import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.net.URLEncoder; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.List; import java.util.Random; +import java.util.stream.Collectors; -import static processing.app.I18n.tr; +import javax.swing.JOptionPane; + +import org.apache.commons.compress.utils.IOUtils; + +import cc.arduino.contributions.SignatureVerifier; +import cc.arduino.utils.FileHash; +import processing.app.legacy.PApplet; /** @@ -51,10 +62,6 @@ */ public class UpdateCheck implements Runnable { Base base; - String downloadURL = tr("https://www.arduino.cc/latest.txt"); - - static final long ONE_DAY = 24 * 60 * 60 * 1000; - public UpdateCheck(Base base) { Thread thread = new Thread(this); @@ -62,21 +69,81 @@ public UpdateCheck(Base base) { thread.start(); } + final long ONE_DAY = 24 * 60 * 60 * 1000; public void run() { - //System.out.println("checking for updates..."); + // Ensure updates-check are made only once per day + Long when = PreferencesData.getLong("update.last"); + long now = System.currentTimeMillis(); + if (when != null && (now - when) < ONE_DAY) { + // don't annoy the shit outta people + return; + } + PreferencesData.setLong("update.last", now); + + checkForIDEUpdates(); - long id; - String idString = PreferencesData.get("update.id"); - if (idString != null) { - id = Long.parseLong(idString); - } else { + checkForSplashImageUpdates(); + } + + private void checkForSplashImageUpdates() { + File tmp = null; + try { + tmp = File.createTempFile("arduino_splash_update", ".txt.asc"); + // Check for updates of the splash screen + downloadFileFromURL("https://go.bug.st/latest_splash.txt.asc", tmp); + SignatureVerifier verifier = new SignatureVerifier(); + if (!verifier.verifyCleartextSignature(tmp)) { + return; + } + String[] lines = verifier.extractTextFromCleartextSignature(tmp); + if (lines.length < 2) { + return; + } + String newSplashUrl = lines[0]; + String checksum = lines[1]; + + // if the splash image has been changed download the new file + String oldSplashUrl = PreferencesData.get("splash.imageurl"); + if (!newSplashUrl.equals(oldSplashUrl)) { + File tmpFile = BaseNoGui.getSettingsFile("splash.png.tmp"); + downloadFileFromURL(newSplashUrl, tmpFile); + + String algo = checksum.split(":")[0]; + String crc = FileHash.hash(tmpFile, algo); + if (!crc.equalsIgnoreCase(checksum)) { + return; + } + + File destFile = BaseNoGui.getSettingsFile("splash.png"); + Files.move(tmpFile.toPath(), destFile.toPath(), + StandardCopyOption.REPLACE_EXISTING); + PreferencesData.set("splash.imageurl", newSplashUrl); + } + + // extend expiration by 24h + long now = System.currentTimeMillis(); + PreferencesData.setLong("splash.expire", now + ONE_DAY); + } catch (Exception e) { + // e.printStackTrace(); + } finally { + if (tmp != null) { + tmp.delete(); + } + } + } + + private void checkForIDEUpdates() { + // Set update id + Long id = PreferencesData.getLong("update.id"); + if (id == null) { // generate a random id in case none exists yet Random r = new Random(); id = r.nextLong(); - PreferencesData.set("update.id", String.valueOf(id)); + PreferencesData.setLong("update.id", id); } + // Check for updates of the IDE try { String info; info = URLEncoder.encode(id + "\t" + @@ -87,18 +154,7 @@ public void run() { System.getProperty("os.version") + "\t" + System.getProperty("os.arch"), "UTF-8"); - int latest = readInt(downloadURL + "?" + info); - - String lastString = PreferencesData.get("update.last"); - long now = System.currentTimeMillis(); - if (lastString != null) { - long when = Long.parseLong(lastString); - if (now - when < ONE_DAY) { - // don't annoy the shit outta people - return; - } - } - PreferencesData.set("update.last", String.valueOf(now)); + int latest = readIntFromURL("https://www.arduino.cc/latest.txt?" + info); String prompt = tr("A new version of Arduino is available,\n" + @@ -116,7 +172,7 @@ public void run() { options, options[0]); if (result == JOptionPane.YES_OPTION) { - Base.openURL(tr("https://www.arduino.cc/en/Main/Software")); + Base.openURL("https://www.arduino.cc/en/Main/Software"); } } } @@ -126,15 +182,38 @@ public void run() { } } + public static File getUpdatedSplashImageFile() { + if (PreferencesData.has("splash.expire")) { + Long expire = PreferencesData.getLong("splash.expire"); + long now = System.currentTimeMillis(); + if (expire != null && now < expire) { + File f = BaseNoGui.getSettingsFile("splash.png"); + if (f.isFile()) { + return f; + } + } + } + return null; + } - protected int readInt(String filename) throws IOException { - URL url = new URL(filename); - BufferedReader reader = null; - try { - reader = new BufferedReader(new InputStreamReader(url.openStream())); - return Integer.parseInt(reader.readLine()); - } finally { - IOUtils.closeQuietly(reader); + protected int readIntFromURL(String _url) throws Exception { + List lines = readFileFromURL(_url); + return Integer.parseInt(lines.get(0)); + } + + protected List readFileFromURL(String _url) throws IOException { + URL url = new URL(_url); + try (BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));) { + return in.lines().collect(Collectors.toList()); + } + } + + protected void downloadFileFromURL(String _url, File dest) throws IOException { + URL url = new URL(_url); + try (InputStream in = url.openStream()) { + try (FileOutputStream out = new FileOutputStream(dest)) { + IOUtils.copy(in, out); + } } } } diff --git a/app/test/cc/arduino/contributions/GPGDetachedSignatureVerifierTest.java b/app/test/cc/arduino/contributions/GPGDetachedSignatureVerifierTest.java deleted file mode 100644 index 7dd7285a064..00000000000 --- a/app/test/cc/arduino/contributions/GPGDetachedSignatureVerifierTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * This file is part of Arduino. - * - * Copyright 2015 Arduino LLC (http://www.arduino.cc/) - * - * Arduino is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - * - * As a special exception, you may use this file as part of a free software - * library without restriction. Specifically, if other files instantiate - * templates or use macros or inline functions from this file, or you compile - * this file and link it with other files to produce an executable, this - * file does not by itself cause the resulting executable to be covered by - * the GNU General Public License. This exception does not however - * invalidate any other reasons why the executable file might be covered by - * the GNU General Public License. - */ - -package cc.arduino.contributions; - -import org.junit.Before; -import org.junit.Test; - -import java.io.File; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class GPGDetachedSignatureVerifierTest { - - private GPGDetachedSignatureVerifier GPGDetachedSignatureVerifier; - - @Before - public void setUp() throws Exception { - GPGDetachedSignatureVerifier = new GPGDetachedSignatureVerifier(); - } - - @Test - public void testSignatureSuccessfulVerification() throws Exception { - File signedFile = new File(GPGDetachedSignatureVerifierTest.class.getResource("./package_index.json").getFile()); - File sign = new File(GPGDetachedSignatureVerifierTest.class.getResource("./package_index.json.sig").getFile()); - File publickKey = new File(GPGDetachedSignatureVerifierTest.class.getResource("./public.gpg.key").getFile()); - assertTrue(GPGDetachedSignatureVerifier.verify(signedFile, sign, publickKey)); - } - - @Test - public void testSignatureFailingVerification() throws Exception { - File fakeSignedFile = File.createTempFile("fakeSigned", "txt"); - fakeSignedFile.deleteOnExit(); - File sign = new File(GPGDetachedSignatureVerifierTest.class.getResource("./package_index.json.sig").getFile()); - File publickKey = new File(GPGDetachedSignatureVerifierTest.class.getResource("./public.gpg.key").getFile()); - assertFalse(GPGDetachedSignatureVerifier.verify(fakeSignedFile, sign, publickKey)); - } -} diff --git a/app/test/cc/arduino/contributions/SignatureVerifierTest.java b/app/test/cc/arduino/contributions/SignatureVerifierTest.java new file mode 100644 index 00000000000..d99c5e15494 --- /dev/null +++ b/app/test/cc/arduino/contributions/SignatureVerifierTest.java @@ -0,0 +1,85 @@ +/* + * This file is part of Arduino. + * + * Copyright 2015 Arduino LLC (http://www.arduino.cc/) + * + * Arduino is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * As a special exception, you may use this file as part of a free software + * library without restriction. Specifically, if other files instantiate + * templates or use macros or inline functions from this file, or you compile + * this file and link it with other files to produce an executable, this + * file does not by itself cause the resulting executable to be covered by + * the GNU General Public License. This exception does not however + * invalidate any other reasons why the executable file might be covered by + * the GNU General Public License. + */ + +package cc.arduino.contributions; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; + +import org.junit.Before; +import org.junit.Test; + +public class SignatureVerifierTest { + + private SignatureVerifier verifier; + + @Before + public void setUp() throws Exception { + verifier = new SignatureVerifier(); + File keyRingFile = new File(SignatureVerifierTest.class.getResource("./test.public.gpg.key").getFile()); + verifier.setKeyRingFile(keyRingFile); + } + + @Test + public void testSignatureSuccessfulVerification() throws Exception { + File signedFile = new File(SignatureVerifierTest.class.getResource("./package_index.json").getFile()); + File sign = new File(SignatureVerifierTest.class.getResource("./package_index.json.sig").getFile()); + assertTrue(verifier.verify(signedFile, sign)); + } + + @Test + public void testSignatureFailingVerification() throws Exception { + File fakeSignedFile = File.createTempFile("fakeSigned", "txt"); + fakeSignedFile.deleteOnExit(); + File sign = new File(SignatureVerifierTest.class.getResource("./package_index.json.sig").getFile()); + assertFalse(verifier.verify(fakeSignedFile, sign)); + } + + @Test + public void testClearTextSignatureVerification() throws Exception { + File signedFile = new File(SignatureVerifierTest.class.getResource("./text.txt.asc").getFile()); + assertTrue(verifier.verifyCleartextSignature(signedFile)); + File signedFile2 = new File(SignatureVerifierTest.class.getResource("./escaped-text.txt.asc").getFile()); + assertTrue(verifier.verifyCleartextSignature(signedFile2)); + File oneBlankLines = new File(SignatureVerifierTest.class.getResource("./one-blank-line.txt.asc").getFile()); + assertTrue(verifier.verifyCleartextSignature(oneBlankLines)); + File twoBlankLines = new File(SignatureVerifierTest.class.getResource("./two-blank-lines.txt.asc").getFile()); + assertTrue(verifier.verifyCleartextSignature(twoBlankLines)); + File noBlankLines = new File(SignatureVerifierTest.class.getResource("./no-blank-lines.txt.asc").getFile()); + assertTrue(verifier.verifyCleartextSignature(noBlankLines)); + } + + @Test + public void testClearTextSignatureFailingVerification() throws Exception { + File signedFile = new File(SignatureVerifierTest.class.getResource("./wrong-text.txt.asc").getFile()); + assertFalse(verifier.verifyCleartextSignature(signedFile)); + } +} diff --git a/app/test/cc/arduino/contributions/escaped-text.txt.asc b/app/test/cc/arduino/contributions/escaped-text.txt.asc new file mode 100644 index 00000000000..9c89d0720c4 --- /dev/null +++ b/app/test/cc/arduino/contributions/escaped-text.txt.asc @@ -0,0 +1,29 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +- - +- - +- - abc +- -- abc +helllo!!!! + + + + + +-----BEGIN PGP SIGNATURE----- + +iQIzBAEBCgAdFiEEMmVnwcayiN8yywYalfpvQ+IRiMQFAl6CcaUACgkQlfpvQ+IR +iMQD9w/+KA2ZusmFnpt7dXTy/6ohNCijMp3pFSVsBzGWBDpGJjYfwek2N8HssXIa +Tq0uzCiTm1Z4kn+SWjIHxKzdihUi9mwkMCqdhNTZN0+FKXc+c358KUYXOlyigOmX +256bG7Ep2P/3ZBzhvVC5WKLIwYhq6cj9fSnt26XP02qt/9ztczGJHDpEfIhPA0LI +PYnUUo4KftQzHp41EPENIyLTkT1YzhypnIHCv2mG8qql+W9blx1eO8gIUGmzQQ0y +CnTY0AIvmrAGd5WQWwKpKy5aLpWDmIW/zSSoDc7jC74i2V6n5Y+Fqq++SVDvIApd +yP+BmL6pDfZf9cV8nDQOI9Jd+/JT3tUct5js9lDhj74g1ZGobx06kZPv/ojbSE42 +jJi75HQ3NDG8dBDMtYzrDeO0QhcpT94LNTdZ8IdR5jCf3I9SkpHB6sb27elVgcBb +stXLRhrf0se2U2pI3CGDkjumm04cDOhY9tLt2CHMlL9yl9LZejyT3xUoLGAy1Ird +Jw6knZM5O/bWzFq1bxlqgz6EspafNy+ZM8/1s+b9ecxKkds7UEZLpOVd+QYMO6i1 +lxm4ZQqRoTiIcVxzhwChd41zxPw9bMNq03prBMnetJ9tvb76qgGcCvRh/ANYWBfa +2zeH3UGaOyMB79oKOSw0qzXkFA+0qXzROObVWTzjGOPZJtkfaA8= +=BqjQ +-----END PGP SIGNATURE----- diff --git a/app/test/cc/arduino/contributions/no-blank-lines.txt.asc b/app/test/cc/arduino/contributions/no-blank-lines.txt.asc new file mode 100644 index 00000000000..9b5e995b022 --- /dev/null +++ b/app/test/cc/arduino/contributions/no-blank-lines.txt.asc @@ -0,0 +1,20 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +hello +-----BEGIN PGP SIGNATURE----- + +iQIzBAEBCgAdFiEEMmVnwcayiN8yywYalfpvQ+IRiMQFAl6C/AsACgkQlfpvQ+IR +iMQYUA//SEHkb60xiXyP8TfTfxiJORr7ltvLtL7N9IwpnrkeKdvvrxC71O5U4kdf +Ek5XUALXUPArWJHByKBZ3saQapRPMUNbsLDG9QqtIffORqgPnfPAgMiGYceIaT1o +QChspIQRL6xt6k8c6o2M5R7CBMjiDqaldpJHsq3kcjcgHBmJFPdDdhjA2D9s4mCo +nymy3PqLcZ4RARrED61VlF4hcVHv5BHPnDZDXug3oGiR2EQTpdQgvBg/rL2xiVHl +MgZ6/5owF6omeNM30JNPQfNdDnV2jDhxILfjY2Rqyj9zClrnZB6qox6h1RoSKIFy +UkBUAs3WapZMqR2UKq3K7dL8cqUKoOFdSpd/R2T4ZBC/6exiwVnbDGa161ur8f4/ +55RxAe/VE+75bC7HI3jNAUn7xGMUGBWgTKh+xTnyh50BHifmCIzyLOgRWds/rvMH +PT0fmtbTWhjrJOfuhi15uSJ0Lvq/XuM/z/aIgeZHaIBnIxvRYUOdP6Rz1xKHmc2q ++puI2FxpbENUyt8TNlrnHJeq72UubVIRJ08CE2iiuWjP+1jFxPYA1tWBYrqtbK7y +2ZPq5c+vy8LMij/SfZkeOa388Ss3lXr5CTJ+O2VMLellyCdUd0G47T26umCr6zaF +jX1huU0rfA+vGLVfr1Z2nexX0r7kwebkz/PWIO1+Kc41gBY+cek= +=Rejh +-----END PGP SIGNATURE----- diff --git a/app/test/cc/arduino/contributions/one-blank-line.txt.asc b/app/test/cc/arduino/contributions/one-blank-line.txt.asc new file mode 100644 index 00000000000..7a48cf72987 --- /dev/null +++ b/app/test/cc/arduino/contributions/one-blank-line.txt.asc @@ -0,0 +1,21 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +hello + +-----BEGIN PGP SIGNATURE----- + +iQIzBAEBCgAdFiEEMmVnwcayiN8yywYalfpvQ+IRiMQFAl6C/E8ACgkQlfpvQ+IR +iMS/iA/+KxhaOTokmXTKgSkZwaCdPwTIdcfIVWMsK6mxXcSeWtmQpCxMyH3sudaA +MvVg8qMHTTiYbFD5TMBF79Eu3nJuLemnm7WbpQMAQ7/Ay8qDrH6MIIqeR6TLa6CO +VQw2a9H3AoqVxBtH0y6vVk+mOnr8NnZA3XoT0B2TuLApqY4uFt9quHBkrycHdXve +6++EyiYeDgY6oG1bAR0S/peFhCL45CId9xJe5RIHT7aFoDKw78LPcfoTLdKCtzzE +CdX1IpzP/tOVlv0LTNPWQQeRQ07BKpquQnQbzuAgVFbLf27kI2e+AxjIdmozEjO9 +rk9Za0tpKYCYT5lF9zfmaCWzkhTfrjB8OPXWYHjozzv4aUhfvgQ+TF9dynWU4ct8 +ACRkb+bp+ENZVy0/1VdDDs3wrFnMHuZyrBWXNI8sBu6f1JSG4ddgy7NfPveOJEdG +wg/4mxzSnBQ7jogRaJLioC+mjxulmN1FCoDMwphOLkmaIX1qIL3OxkEBfSzpfkfe +wAYhjSqLHHXwx0W0resDahC2L81gBnTbf0yP1jEWjnSs96AqwvLSAj0gyC4BtVoN +LSgYDFeNitoBMDx8osi0KjLydOzbQW9L5u//Rm/Q/8/p7+4u/X3X8EWHULHIsrRW +T9M6jun8zTr9OLAk9W1T76Yu2Wyu+ps6f6iAeSlvqTTpr3+j2W4= +=0JE+ +-----END PGP SIGNATURE----- diff --git a/app/test/cc/arduino/contributions/public.gpg.key b/app/test/cc/arduino/contributions/test.public.gpg.key similarity index 62% rename from app/test/cc/arduino/contributions/public.gpg.key rename to app/test/cc/arduino/contributions/test.public.gpg.key index 5de39fed1f7..fde092a92ac 100644 Binary files a/app/test/cc/arduino/contributions/public.gpg.key and b/app/test/cc/arduino/contributions/test.public.gpg.key differ diff --git a/app/test/cc/arduino/contributions/text.txt.asc b/app/test/cc/arduino/contributions/text.txt.asc new file mode 100644 index 00000000000..23b47f73ba3 --- /dev/null +++ b/app/test/cc/arduino/contributions/text.txt.asc @@ -0,0 +1,21 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +helllo!!!! + +-----BEGIN PGP SIGNATURE----- + +iQIzBAEBCgAdFiEEMmVnwcayiN8yywYalfpvQ+IRiMQFAl6CPtwACgkQlfpvQ+IR +iMS8AA//dCIbYCPwTdOFf3YRFO2Z7YSgAgEpctcH1UW9IaZpGlfZDqiskgNT5zWx +ooW0EqVagyyY+1Fips4a52/urQmbQ+0RIU6r4zDUvyMoN/f41jFKd1eY365oqORo +G5avMFhZ7PwbYXKhDeVQrDCRSYSvWYyRXH97VpFvo/SSuvB7KWvlTFFK9rt1xysx +rjikCqMWuPeReSclLCMCGQGjcJExNsu7E9l55NcJMOP8a3yVlY9b1LwH5nuVXOfw +UvSzHK2K16O3COb0otWN8VZHB0x2y3Y1boIF2J/Wt9zBaB/d4cmacwL4KLHiw7KR +q5YzDfEpmwMM9S5QLsLPYBWr9B1XSQ+PrylPSha1NnDStr/RkFtwVsags3igJu5E +Ye+aJra6D/VREFc/IQYsEmcDHYdKp3CNJn/BdNwTDI4BIdj4lBDd6lVrcL+Id8pl +ivdk1bV/575eTH9cDf65aF/lmd/z6dGNLcke4N76n8g/xjAuSkN7FDm91KGv9oCJ +e0eO0+GDJbfMHz61vQ1DPfUWqyQLC3z9OeX+bUvqSb1Xy9i2OI0FlY3GMtwDONto +qShjcbeXXYt74bINsAEV78pSoYPHc2rYco1/qrAL1bYY9Hngy3FvgD7mVNd7nC/J +buFQ4Ek07urJpA9lw8o/z49O7iGisc8UXuPeHY2z7LmUuCvKpKI= +=DKVh +-----END PGP SIGNATURE----- diff --git a/app/test/cc/arduino/contributions/two-blank-lines.txt.asc b/app/test/cc/arduino/contributions/two-blank-lines.txt.asc new file mode 100644 index 00000000000..ec8f2f5b18c --- /dev/null +++ b/app/test/cc/arduino/contributions/two-blank-lines.txt.asc @@ -0,0 +1,22 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +hello + + +-----BEGIN PGP SIGNATURE----- + +iQIzBAEBCgAdFiEEMmVnwcayiN8yywYalfpvQ+IRiMQFAl6C/H8ACgkQlfpvQ+IR +iMQD9RAAk34n7WegdXdreKLU5nAkx9/lcPwZYRcwTeV6pgroxQ3XuC3oEqUwDQtD +RLT+Gc+1j7XMGxTIJtHhpwpJk2IeT5x/lfqpMwpHoJUEOA/QZ7YtUTUj5aqc3bJV +QEWXrNyceqaYkUguR7gk0ahtniTuaj9WaixRpxiu9+AMht74g0iZm99h2fpgiIb1 +7M2SdMbmyW1mK7BFN9Cghz/8NedV6TeKpQnWWpN+hdO1fsjGIVRNRfTMod7gL6QK +KhPa3eQW+yxlEshKZwQJwIe3vcgquK6bAl/p20EU7+2ytnFd3zBhcCFJtC6fV+zF +s89BOAoAJPRaBKbQp01IsBB5mNZUxKFOa+e94tFLmaaI7KhHB7oQ7Qnx+hTgq7bG +9PRza0bHMkBPumACM8FNjOlzJNY5eTnfDcAq2kgaAdoX5LFLrIiYUNtz9gmIIHNj +xekG9LB3WUv013jPSOUnbmGkch0VvVgFvcsOtGWTHp+VlZLOWZjXkO+Cco2qWcVr +F7ckPTXs+nzoOTVsYZLfN8g92y2sT548+Q9bCtT1QStGCSTyNF1o0JW9Ih1hsSkn +qJVgo8wGO45mTT4GJ6VWUkFkAjoaZM14F70CJp0eLw5PSxDIkSsmkL7CYwGeA+6N +UtKIJ1ZoFMElW5bMuSayLEjtC9RkGILjplkiFY+h7gJd/vitTvI= +=g/9c +-----END PGP SIGNATURE----- diff --git a/app/test/cc/arduino/contributions/wrong-text.txt.asc b/app/test/cc/arduino/contributions/wrong-text.txt.asc new file mode 100644 index 00000000000..de08872c6be --- /dev/null +++ b/app/test/cc/arduino/contributions/wrong-text.txt.asc @@ -0,0 +1,21 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +!!helllo!!!! + +-----BEGIN PGP SIGNATURE----- + +iQIzBAEBCgAdFiEEMmVnwcayiN8yywYalfpvQ+IRiMQFAl6CPtwACgkQlfpvQ+IR +iMS8AA//dCIbYCPwTdOFf3YRFO2Z7YSgAgEpctcH1UW9IaZpGlfZDqiskgNT5zWx +ooW0EqVagyyY+1Fips4a52/urQmbQ+0RIU6r4zDUvyMoN/f41jFKd1eY365oqORo +G5avMFhZ7PwbYXKhDeVQrDCRSYSvWYyRXH97VpFvo/SSuvB7KWvlTFFK9rt1xysx +rjikCqMWuPeReSclLCMCGQGjcJExNsu7E9l55NcJMOP8a3yVlY9b1LwH5nuVXOfw +UvSzHK2K16O3COb0otWN8VZHB0x2y3Y1boIF2J/Wt9zBaB/d4cmacwL4KLHiw7KR +q5YzDfEpmwMM9S5QLsLPYBWr9B1XSQ+PrylPSha1NnDStr/RkFtwVsags3igJu5E +Ye+aJra6D/VREFc/IQYsEmcDHYdKp3CNJn/BdNwTDI4BIdj4lBDd6lVrcL+Id8pl +ivdk1bV/575eTH9cDf65aF/lmd/z6dGNLcke4N76n8g/xjAuSkN7FDm91KGv9oCJ +e0eO0+GDJbfMHz61vQ1DPfUWqyQLC3z9OeX+bUvqSb1Xy9i2OI0FlY3GMtwDONto +qShjcbeXXYt74bINsAEV78pSoYPHc2rYco1/qrAL1bYY9Hngy3FvgD7mVNd7nC/J +buFQ4Ek07urJpA9lw8o/z49O7iGisc8UXuPeHY2z7LmUuCvKpKI= +=DKVh +-----END PGP SIGNATURE----- diff --git a/arduino-core/src/cc/arduino/contributions/GPGDetachedSignatureVerifier.java b/arduino-core/src/cc/arduino/contributions/GPGDetachedSignatureVerifier.java deleted file mode 100644 index ead276f4731..00000000000 --- a/arduino-core/src/cc/arduino/contributions/GPGDetachedSignatureVerifier.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * This file is part of Arduino. - * - * Copyright 2015 Arduino LLC (http://www.arduino.cc/) - * - * Arduino is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - * - * As a special exception, you may use this file as part of a free software - * library without restriction. Specifically, if other files instantiate - * templates or use macros or inline functions from this file, or you compile - * this file and link it with other files to produce an executable, this - * file does not by itself cause the resulting executable to be covered by - * the GNU General Public License. This exception does not however - * invalidate any other reasons why the executable file might be covered by - * the GNU General Public License. - */ - -package cc.arduino.contributions; - -import org.apache.commons.compress.utils.IOUtils; -import org.bouncycastle.openpgp.*; -import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; -import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; - -import java.io.*; -import java.util.Iterator; - -public class GPGDetachedSignatureVerifier extends SignatureVerifier { - - private String keyId; - - public GPGDetachedSignatureVerifier() { - this("7F294291"); - } - - public GPGDetachedSignatureVerifier(String keyId) { - this.keyId = keyId; - } - - @Override - protected boolean verify(File signedFile, File signature, File publicKey) throws IOException { - FileInputStream signatureInputStream = null; - FileInputStream signedFileInputStream = null; - try { - signatureInputStream = new FileInputStream(signature); - PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(signatureInputStream, new BcKeyFingerprintCalculator()); - - Object nextObject; - try { - nextObject = pgpObjectFactory.nextObject(); - if (!(nextObject instanceof PGPSignatureList)) { - return false; - } - } catch (IOException e) { - return false; - } - PGPSignatureList pgpSignatureList = (PGPSignatureList) nextObject; - assert pgpSignatureList.size() == 1; - PGPSignature pgpSignature = pgpSignatureList.get(0); - - PGPPublicKey pgpPublicKey = readPublicKey(publicKey, keyId); - - pgpSignature.init(new BcPGPContentVerifierBuilderProvider(), pgpPublicKey); - signedFileInputStream = new FileInputStream(signedFile); - pgpSignature.update(IOUtils.toByteArray(signedFileInputStream)); - - return pgpSignature.verify(); - } catch (PGPException e) { - throw new IOException(e); - } finally { - IOUtils.closeQuietly(signatureInputStream); - IOUtils.closeQuietly(signedFileInputStream); - } - } - - private PGPPublicKey readPublicKey(File file, String id) throws IOException, PGPException { - InputStream keyIn = null; - try { - keyIn = new BufferedInputStream(new FileInputStream(file)); - return readPublicKey(keyIn, id); - } finally { - IOUtils.closeQuietly(keyIn); - } - } - - private PGPPublicKey readPublicKey(InputStream input, String id) throws IOException, PGPException { - PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(PGPUtil.getDecoderStream(input), new BcKeyFingerprintCalculator()); - - Iterator keyRingIter = pgpPub.getKeyRings(); - while (keyRingIter.hasNext()) { - PGPPublicKeyRing keyRing = keyRingIter.next(); - - Iterator keyIter = keyRing.getPublicKeys(); - while (keyIter.hasNext()) { - PGPPublicKey key = keyIter.next(); - - if (Long.toHexString(key.getKeyID()).toUpperCase().endsWith(id)) { - return key; - } - } - } - - throw new IllegalArgumentException("Can't find encryption key in key ring."); - } - -} diff --git a/arduino-core/src/cc/arduino/contributions/SignatureVerifier.java b/arduino-core/src/cc/arduino/contributions/SignatureVerifier.java index a4ea7a7ba53..940e38666c1 100644 --- a/arduino-core/src/cc/arduino/contributions/SignatureVerifier.java +++ b/arduino-core/src/cc/arduino/contributions/SignatureVerifier.java @@ -29,12 +29,38 @@ package cc.arduino.contributions; -import processing.app.BaseNoGui; - import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.compress.utils.IOUtils; +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory; +import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; + +import processing.app.BaseNoGui; + +public class SignatureVerifier { + + private File keyRingFile; + + public SignatureVerifier() { + keyRingFile = new File(BaseNoGui.getContentFile("lib"), "public.gpg.key"); + } -public abstract class SignatureVerifier { + public void setKeyRingFile(File keyRingFile) { + this.keyRingFile = keyRingFile; + } public boolean isSigned(File indexFile) { File signature = new File(indexFile.getParent(), indexFile.getName() + ".sig"); @@ -43,7 +69,7 @@ public boolean isSigned(File indexFile) { } try { - return verify(indexFile, signature, new File(BaseNoGui.getContentFile("lib"), "public.gpg.key")); + return verify(indexFile, signature); } catch (Exception e) { BaseNoGui.showWarning(e.getMessage(), e.getMessage(), e); return false; @@ -52,13 +78,122 @@ public boolean isSigned(File indexFile) { public boolean isSigned(File indexFile, File signature) { try { - return verify(indexFile, signature, new File(BaseNoGui.getContentFile("lib"), "public.gpg.key")); + return verify(indexFile, signature); } catch (Exception e) { BaseNoGui.showWarning(e.getMessage(), e.getMessage(), e); return false; } } - protected abstract boolean verify(File signedFile, File signature, File publicKey) throws IOException; + protected boolean verify(File signedFile, File signatureFile) throws IOException { + try { + // Read signature from signatureFile + PGPSignature signature; + try (FileInputStream in = new FileInputStream(signatureFile)) { + PGPObjectFactory objFactory = new PGPObjectFactory(in, new BcKeyFingerprintCalculator()); + Object obj = objFactory.nextObject(); + if (!(obj instanceof PGPSignatureList)) { + return false; + } + PGPSignatureList signatureList = (PGPSignatureList) obj; + if (signatureList.size() != 1) { + return false; + } + signature = signatureList.get(0); + } catch (Exception e) { + return false; + } + + // Extract public key from keyring + PGPPublicKey pgpPublicKey = readPublicKey(signature.getKeyID()); + // Check signature + signature.init(new BcPGPContentVerifierBuilderProvider(), pgpPublicKey); + try (FileInputStream in = new FileInputStream(signedFile)) { + signature.update(IOUtils.toByteArray(in)); + return signature.verify(); + } + } catch (PGPException e) { + throw new IOException(e); + } + } + + private PGPPublicKey readPublicKey(long id) throws IOException, PGPException { + try (InputStream in = PGPUtil.getDecoderStream(new FileInputStream(keyRingFile))) { + PGPPublicKeyRingCollection pubRing = new PGPPublicKeyRingCollection(in, new BcKeyFingerprintCalculator()); + + PGPPublicKey publicKey = pubRing.getPublicKey(id); + if (publicKey == null) { + throw new IllegalArgumentException("Can't find public key in key ring."); + } + return publicKey; + } + } + + public String[] extractTextFromCleartextSignature(File inFile) throws FileNotFoundException, IOException { + try (ArmoredInputStream in = new ArmoredInputStream(new FileInputStream(inFile))) { + return extractTextFromCleartextSignature(in); + } + } + + public boolean verifyCleartextSignature(File inFile) { + try (ArmoredInputStream in = new ArmoredInputStream(new FileInputStream(inFile))) { + String[] clearTextLines = extractTextFromCleartextSignature(in); + int clearTextSize = clearTextLines.length; + + JcaPGPObjectFactory pgpFact = new JcaPGPObjectFactory(in); + PGPSignatureList p3 = (PGPSignatureList) pgpFact.nextObject(); + PGPSignature sig = p3.get(0); + PGPPublicKey publicKey = readPublicKey(sig.getKeyID()); + + sig.init(new BcPGPContentVerifierBuilderProvider(), publicKey); + for (int i = 0; i < clearTextSize; i++) { + sig.update(clearTextLines[i].getBytes()); + if (i + 1 < clearTextSize) { + // https://tools.ietf.org/html/rfc4880#section-7 + // Convert all line endings to '\r\n' + sig.update((byte) '\r'); + sig.update((byte) '\n'); + } + } + return sig.verify(); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + private String[] extractTextFromCleartextSignature(ArmoredInputStream in) throws FileNotFoundException, IOException { + // https://tools.ietf.org/html/rfc4880#section-7 + // ArmoredInputStream does unescape dash-escaped string in armored text and skips + // all headers. To calculate the signature we still need to: + // 1. handle different line endings \n or \n\r or \r\n + // 2. remove trailing whitespaces from each line (' ' and '\t') + // 3. remove the latest line ending + + String clearText = ""; + for (;;) { + int c = in.read(); + // in.isClearText() refers to the PREVIOUS byte read + if (c == -1 || !in.isClearText()) { + break; + } + // 1. convert all line endings to '\r\n' + if (c == '\r') { + continue; + } + clearText += (char) c; + } + + // 3. remove the latest line ending + if (clearText.endsWith("\n")) { + clearText = clearText.substring(0, clearText.length() - 1); + } + String[] lines = clearText.split("\n", -1); + for (int i = 0; i < lines.length; i++) { + // 2. remove trailing whitespaces from each line (' ' and '\t') + lines[i] = lines[i].replaceAll("[ \\t]+$", ""); + } + return lines; + } } diff --git a/arduino-core/src/cc/arduino/contributions/libraries/LibraryInstaller.java b/arduino-core/src/cc/arduino/contributions/libraries/LibraryInstaller.java index 3f00f909b0d..858734fd88b 100644 --- a/arduino-core/src/cc/arduino/contributions/libraries/LibraryInstaller.java +++ b/arduino-core/src/cc/arduino/contributions/libraries/LibraryInstaller.java @@ -31,7 +31,7 @@ import cc.arduino.Constants; import cc.arduino.contributions.DownloadableContributionsDownloader; -import cc.arduino.contributions.GPGDetachedSignatureVerifier; +import cc.arduino.contributions.SignatureVerifier; import cc.arduino.contributions.GZippedJsonDownloader; import cc.arduino.contributions.ProgressListener; import cc.arduino.utils.ArchiveExtractor; @@ -60,9 +60,9 @@ public class LibraryInstaller { private static Logger log = LogManager.getLogger(LibraryInstaller.class); private final Platform platform; - private final GPGDetachedSignatureVerifier signatureVerifier; + private final SignatureVerifier signatureVerifier; - public LibraryInstaller(Platform platform, GPGDetachedSignatureVerifier signatureVerifier) { + public LibraryInstaller(Platform platform, SignatureVerifier signatureVerifier) { this.platform = platform; this.signatureVerifier = signatureVerifier; } diff --git a/arduino-core/src/cc/arduino/contributions/packages/ContributionInstaller.java b/arduino-core/src/cc/arduino/contributions/packages/ContributionInstaller.java index 2b6ff4cdea8..a58ee552691 100644 --- a/arduino-core/src/cc/arduino/contributions/packages/ContributionInstaller.java +++ b/arduino-core/src/cc/arduino/contributions/packages/ContributionInstaller.java @@ -32,8 +32,8 @@ import cc.arduino.Constants; import cc.arduino.contributions.DownloadableContribution; import cc.arduino.contributions.DownloadableContributionsDownloader; -import cc.arduino.contributions.ProgressListener; import cc.arduino.contributions.SignatureVerifier; +import cc.arduino.contributions.ProgressListener; import cc.arduino.filters.FileExecutablePredicate; import cc.arduino.utils.ArchiveExtractor; import cc.arduino.utils.MultiStepProgress; diff --git a/arduino-core/src/processing/app/BaseNoGui.java b/arduino-core/src/processing/app/BaseNoGui.java index c47a82d69b8..dc8bd7091a0 100644 --- a/arduino-core/src/processing/app/BaseNoGui.java +++ b/arduino-core/src/processing/app/BaseNoGui.java @@ -1,7 +1,7 @@ package processing.app; import cc.arduino.Constants; -import cc.arduino.contributions.GPGDetachedSignatureVerifier; +import cc.arduino.contributions.SignatureVerifier; import cc.arduino.contributions.VersionComparator; import cc.arduino.contributions.libraries.LibrariesIndexer; import cc.arduino.contributions.packages.ContributedPlatform; @@ -477,7 +477,7 @@ static public void initLogger() { static public void initPackages() throws Exception { indexer = new ContributionsIndexer(getSettingsFolder(), getHardwareFolder(), getPlatform(), - new GPGDetachedSignatureVerifier()); + new SignatureVerifier()); try { indexer.parseIndex(); diff --git a/arduino-core/src/processing/app/PreferencesData.java b/arduino-core/src/processing/app/PreferencesData.java index 01f4568ad5b..162c4090224 100644 --- a/arduino-core/src/processing/app/PreferencesData.java +++ b/arduino-core/src/processing/app/PreferencesData.java @@ -282,4 +282,16 @@ public static boolean areInsecurePackagesAllowed() { } return getBoolean(Constants.PREF_CONTRIBUTIONS_TRUST_ALL, false); } + + public static void setLong(String k, long v) { + set(k, String.valueOf(v)); + } + + public static Long getLong(String k) { + try { + return Long.parseLong(get(k)); + } catch (NumberFormatException e) { + return null; + } + } } diff --git a/build/shared/lib/public.gpg.key b/build/shared/lib/public.gpg.key index 5de39fed1f7..fde092a92ac 100644 Binary files a/build/shared/lib/public.gpg.key and b/build/shared/lib/public.gpg.key differ