Skip to content

Dynamic splash image (take 2) #9935

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 15 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.xml
Original file line number Diff line number Diff line change
@@ -101,6 +101,7 @@
<copy todir="test-bin" overwrite="true" verbose="true">
<fileset dir="test" includes="**/*.zip" />
<fileset dir="test" includes="**/*.txt" />
<fileset dir="test" includes="**/*.txt.asc" />
<fileset dir="test" includes="**/*.properties" />
<fileset dir="test" includes="**/*.ino" />
<fileset dir="test" includes="**/*.json*" />
21 changes: 20 additions & 1 deletion app/src/cc/arduino/view/SplashScreenHelper.java
Original file line number Diff line number Diff line change
@@ -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();
}
}

}
14 changes: 11 additions & 3 deletions app/src/processing/app/Base.java
Original file line number Diff line number Diff line change
@@ -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);
35 changes: 27 additions & 8 deletions app/src/processing/app/Theme.java
Original file line number Diff line number Diff line change
@@ -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));
}

/**
151 changes: 115 additions & 36 deletions app/src/processing/app/UpdateCheck.java
Original file line number Diff line number Diff line change
@@ -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,32 +62,88 @@
*/
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);
this.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<String> lines = readFileFromURL(_url);
return Integer.parseInt(lines.get(0));
}

protected List<String> 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);
}
}
}
}

This file was deleted.

85 changes: 85 additions & 0 deletions app/test/cc/arduino/contributions/SignatureVerifierTest.java
Original file line number Diff line number Diff line change
@@ -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));
}
}
29 changes: 29 additions & 0 deletions app/test/cc/arduino/contributions/escaped-text.txt.asc
Original file line number Diff line number Diff line change
@@ -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-----
20 changes: 20 additions & 0 deletions app/test/cc/arduino/contributions/no-blank-lines.txt.asc
Original file line number Diff line number Diff line change
@@ -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-----
21 changes: 21 additions & 0 deletions app/test/cc/arduino/contributions/one-blank-line.txt.asc
Original file line number Diff line number Diff line change
@@ -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-----
Binary file not shown.
21 changes: 21 additions & 0 deletions app/test/cc/arduino/contributions/text.txt.asc
Original file line number Diff line number Diff line change
@@ -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-----
22 changes: 22 additions & 0 deletions app/test/cc/arduino/contributions/two-blank-lines.txt.asc
Original file line number Diff line number Diff line change
@@ -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-----
21 changes: 21 additions & 0 deletions app/test/cc/arduino/contributions/wrong-text.txt.asc
Original file line number Diff line number Diff line change
@@ -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-----

This file was deleted.

147 changes: 141 additions & 6 deletions arduino-core/src/cc/arduino/contributions/SignatureVerifier.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 2 additions & 2 deletions arduino-core/src/processing/app/BaseNoGui.java
Original file line number Diff line number Diff line change
@@ -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();
12 changes: 12 additions & 0 deletions arduino-core/src/processing/app/PreferencesData.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Binary file modified build/shared/lib/public.gpg.key
Binary file not shown.