diff --git a/.gitignore b/.gitignore
index 80079e7e..33cfa301 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,6 +31,7 @@ frontend/qml/*.qmlc
/bridge_*_*.tgz
/ie_*_*.tgz
/versioner
+/hasher
cmd/Desktop-Bridge/deploy
cmd/Import-Export/deploy
internal/frontend/qt*/moc.cpp
diff --git a/Makefile b/Makefile
index efea918f..133128fd 100644
--- a/Makefile
+++ b/Makefile
@@ -7,7 +7,7 @@ TARGET_CMD?=Desktop-Bridge
TARGET_OS?=${GOOS}
## Build
-.PHONY: build build-ie build-nogui build-ie-nogui build-launcher build-launcher-ie versioner
+.PHONY: build build-ie build-nogui build-ie-nogui build-launcher build-launcher-ie versioner hasher
# Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=1.5.5+git
@@ -87,7 +87,10 @@ build-launcher-ie:
go build -ldflags="-X 'main.ConfigName=importExport' -X 'main.ExeName=Import-Export'" -o launcher-ie cmd/launcher/main.go
versioner:
- go build ${BUILD_FLAGS} ${GO_LDFLAGS} -o versioner cmd/versioner/main.go
+ go build ${BUILD_FLAGS} ${GO_LDFLAGS} -o versioner utils/versioner/main.go
+
+hasher:
+ go build -o hasher utils/hasher/main.go
${TGZ_TARGET}: ${DEPLOY_DIR}/${TARGET_OS}
rm -f $@
diff --git a/cmd/launcher/main.go b/cmd/launcher/main.go
index c0ca82ca..cc69661c 100644
--- a/cmd/launcher/main.go
+++ b/cmd/launcher/main.go
@@ -149,7 +149,12 @@ func getPathToExecutable(name string, versioner *versioner.Versioner, kr *crypto
vlog := logrus.WithField("version", version)
if err := version.VerifyFiles(kr); err != nil {
- vlog.WithError(err).Error("Failed to verify files")
+ vlog.WithError(err).Error("Files failed verification and will be removed")
+
+ if err := version.Remove(); err != nil {
+ vlog.WithError(err).Error("Failed to remove files")
+ }
+
continue
}
diff --git a/internal/versioner/version.go b/internal/versioner/version.go
index 4e64863e..b263ed3c 100644
--- a/internal/versioner/version.go
+++ b/internal/versioner/version.go
@@ -18,14 +18,19 @@
package versioner
import (
+ "bytes"
+ "errors"
"io/ioutil"
"os"
"path/filepath"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gopenpgp/v2/crypto"
+ "github.com/ProtonMail/proton-bridge/pkg/sum"
)
+const sumFile = ".sum"
+
type Version struct {
version *semver.Version
path string
@@ -47,31 +52,34 @@ func (v Versions) Swap(i, j int) {
// VerifyFiles verifies all files in the version directory.
func (v *Version) VerifyFiles(kr *crypto.KeyRing) error {
- return filepath.Walk(v.path, func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return err
- }
+ fileBytes, err := ioutil.ReadFile(filepath.Join(v.path, sumFile)) // nolint[gosec]
+ if err != nil {
+ return err
+ }
- if filepath.Ext(path) == ".sig" || info.IsDir() {
- return nil
- }
+ sigBytes, err := ioutil.ReadFile(filepath.Join(v.path, sumFile+".sig")) // nolint[gosec]
+ if err != nil {
+ return err
+ }
- fileBytes, err := ioutil.ReadFile(path) // nolint[gosec]
- if err != nil {
- return err
- }
+ if err := kr.VerifyDetached(
+ crypto.NewPlainMessage(fileBytes),
+ crypto.NewPGPSignature(sigBytes),
+ crypto.GetUnixTime(),
+ ); err != nil {
+ return err
+ }
- sigBytes, err := ioutil.ReadFile(path + ".sig") // nolint[gosec]
- if err != nil {
- return err
- }
+ sum, err := sum.RecursiveSum(v.path, sumFile)
+ if err != nil {
+ return err
+ }
- return kr.VerifyDetached(
- crypto.NewPlainMessage(fileBytes),
- crypto.NewPGPSignature(sigBytes),
- crypto.GetUnixTime(),
- )
- })
+ if !bytes.Equal(sum, fileBytes) {
+ return errors.New("sum mismatch")
+ }
+
+ return nil
}
// GetExecutable returns the full path to the executable of the given version.
@@ -85,3 +93,8 @@ func (v *Version) GetExecutable(name string) (string, error) {
return exe, nil
}
+
+// Remove removes this version directory.
+func (v *Version) Remove() error {
+ return os.RemoveAll(v.path)
+}
diff --git a/internal/versioner/version_test.go b/internal/versioner/version_test.go
index 0fe84b70..b42799e7 100644
--- a/internal/versioner/version_test.go
+++ b/internal/versioner/version_test.go
@@ -26,6 +26,7 @@ import (
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gopenpgp/v2/crypto"
+ "github.com/ProtonMail/proton-bridge/pkg/sum"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -39,12 +40,12 @@ func TestVerifyFiles(t *testing.T) {
path: tempDir,
}
- kr := createSignedFiles(t,
- filepath.Join(tempDir, "f1.txt"),
- filepath.Join(tempDir, "f2.png"),
- filepath.Join(tempDir, "f3.dat"),
- filepath.Join(tempDir, "sub", "f4.tar"),
- filepath.Join(tempDir, "sub", "f5.tgz"),
+ kr := createSignedFiles(t, tempDir,
+ "f1.txt",
+ "f2.png",
+ "f3.dat",
+ filepath.Join("sub", "f4.tar"),
+ filepath.Join("sub", "f5.tgz"),
)
assert.NoError(t, version.VerifyFiles(kr))
@@ -59,12 +60,12 @@ func TestVerifyWithBadFile(t *testing.T) {
path: tempDir,
}
- kr := createSignedFiles(t,
- filepath.Join(tempDir, "f1.txt"),
- filepath.Join(tempDir, "f2.png"),
- filepath.Join(tempDir, "f3.bad"),
- filepath.Join(tempDir, "sub", "f4.tar"),
- filepath.Join(tempDir, "sub", "f5.tgz"),
+ kr := createSignedFiles(t, tempDir,
+ "f1.txt",
+ "f2.png",
+ "f3.bad",
+ filepath.Join("sub", "f4.tar"),
+ filepath.Join("sub", "f5.tgz"),
)
badKeyRing := makeKeyRing(t)
@@ -82,12 +83,12 @@ func TestVerifyWithBadSubFile(t *testing.T) {
path: tempDir,
}
- kr := createSignedFiles(t,
- filepath.Join(tempDir, "f1.txt"),
- filepath.Join(tempDir, "f2.png"),
- filepath.Join(tempDir, "f3.dat"),
- filepath.Join(tempDir, "sub", "f4.tar"),
- filepath.Join(tempDir, "sub", "f5.bad"),
+ kr := createSignedFiles(t, tempDir,
+ "f1.txt",
+ "f2.png",
+ "f3.dat",
+ filepath.Join("sub", "f4.tar"),
+ filepath.Join("sub", "f5.bad"),
)
badKeyRing := makeKeyRing(t)
@@ -96,15 +97,24 @@ func TestVerifyWithBadSubFile(t *testing.T) {
assert.Error(t, version.VerifyFiles(kr))
}
-func createSignedFiles(t *testing.T, paths ...string) *crypto.KeyRing {
+func createSignedFiles(t *testing.T, root string, paths ...string) *crypto.KeyRing {
kr := makeKeyRing(t)
for _, path := range paths {
- require.NoError(t, os.MkdirAll(filepath.Dir(path), 0700))
- makeFile(t, path)
- signFile(t, path, kr)
+ makeFile(t, filepath.Join(root, path))
}
+ sum, err := sum.RecursiveSum(root, "")
+ require.NoError(t, err)
+
+ sumFile, err := os.Create(filepath.Join(root, sumFile))
+ require.NoError(t, err)
+
+ _, err = sumFile.Write(sum)
+ require.NoError(t, err)
+
+ signFile(t, sumFile.Name(), kr)
+
return kr
}
@@ -119,6 +129,8 @@ func makeKeyRing(t *testing.T) *crypto.KeyRing {
}
func makeFile(t *testing.T, path string) {
+ require.NoError(t, os.MkdirAll(filepath.Dir(path), 0700))
+
f, err := os.Create(path)
require.NoError(t, err)
diff --git a/internal/versioner/versioner_remove_test.go b/internal/versioner/versioner_remove_test.go
new file mode 100644
index 00000000..f2c344b6
--- /dev/null
+++ b/internal/versioner/versioner_remove_test.go
@@ -0,0 +1,52 @@
+// Copyright (c) 2020 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail Bridge 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 3 of the License, or
+// (at your option) any later version.
+//
+// ProtonMail Bridge 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 ProtonMail Bridge. If not, see .
+
+// +build !darwin
+
+package versioner
+
+import (
+ "io/ioutil"
+ "path/filepath"
+ "testing"
+
+ "github.com/Masterminds/semver/v3"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// RemoveOldVersions is a noop on darwin; we don't test it there.
+
+func TestRemoveOldVersions(t *testing.T) {
+ updates, err := ioutil.TempDir("", "updates")
+ require.NoError(t, err)
+
+ v := newTestVersioner(t, "myCoolApp", updates, "2.3.4-beta", "2.3.4", "2.3.5", "2.4.0")
+
+ allVersions, err := v.ListVersions()
+ require.NoError(t, err)
+ require.Len(t, allVersions, 4)
+
+ assert.NoError(t, v.RemoveOldVersions())
+
+ cleanedVersions, err := v.ListVersions()
+ assert.NoError(t, err)
+ assert.Len(t, cleanedVersions, 1)
+
+ assert.Equal(t, semver.MustParse("2.4.0"), cleanedVersions[0].version)
+ assert.Equal(t, filepath.Join(updates, "2.4.0"), cleanedVersions[0].path)
+}
diff --git a/internal/versioner/versioner_test.go b/internal/versioner/versioner_test.go
index c8d12eb8..dd75c209 100644
--- a/internal/versioner/versioner_test.go
+++ b/internal/versioner/versioner_test.go
@@ -50,26 +50,6 @@ func TestListVersions(t *testing.T) {
assert.Equal(t, filepath.Join(updates, "2.3.4-beta"), versions[3].path)
}
-func TestRemoveOldVersions(t *testing.T) {
- updates, err := ioutil.TempDir("", "updates")
- require.NoError(t, err)
-
- v := newTestVersioner(t, "myCoolApp", updates, "2.3.4-beta", "2.3.4", "2.3.5", "2.4.0")
-
- allVersions, err := v.ListVersions()
- require.NoError(t, err)
- require.Len(t, allVersions, 4)
-
- assert.NoError(t, v.RemoveOldVersions())
-
- cleanedVersions, err := v.ListVersions()
- assert.NoError(t, err)
- assert.Len(t, cleanedVersions, 1)
-
- assert.Equal(t, semver.MustParse("2.4.0"), cleanedVersions[0].version)
- assert.Equal(t, filepath.Join(updates, "2.4.0"), cleanedVersions[0].path)
-}
-
func newTestVersioner(t *testing.T, exeName, updates string, versions ...string) *Versioner {
for _, version := range versions {
makeDummyVersionDirectory(t, exeName, updates, version)
diff --git a/pkg/sum/sum.go b/pkg/sum/sum.go
new file mode 100644
index 00000000..d3d3d9e7
--- /dev/null
+++ b/pkg/sum/sum.go
@@ -0,0 +1,66 @@
+// Copyright (c) 2020 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail Bridge 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 3 of the License, or
+// (at your option) any later version.
+//
+// ProtonMail Bridge 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 ProtonMail Bridge. If not, see .
+
+package sum
+
+import (
+ "crypto/sha512"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// RecursiveSum computes the sha512 sum of all files in the root directory and descendents.
+// If a skipFile is provided (e.g. the path of a checksum file relative to rootDir), it (and its signature) is ignored.
+func RecursiveSum(rootDir, skipFile string) ([]byte, error) {
+ hash := sha512.New()
+
+ if err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+
+ if info.IsDir() {
+ return nil
+ }
+
+ // The hashfile itself isn't included in the hash.
+ if path == filepath.Join(rootDir, skipFile) || path == filepath.Join(rootDir, skipFile+".sig") {
+ return nil
+ }
+
+ if _, err := hash.Write([]byte(strings.TrimPrefix(path, rootDir))); err != nil {
+ return err
+ }
+
+ f, err := os.Open(path) // nolint[gosec]
+ if err != nil {
+ return err
+ }
+
+ if _, err := io.Copy(hash, f); err != nil {
+ return err
+ }
+
+ return nil
+ }); err != nil {
+ return nil, err
+ }
+
+ return hash.Sum([]byte{}), nil
+}
diff --git a/pkg/sum/sum_test.go b/pkg/sum/sum_test.go
new file mode 100644
index 00000000..d616f543
--- /dev/null
+++ b/pkg/sum/sum_test.go
@@ -0,0 +1,112 @@
+// Copyright (c) 2020 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail Bridge 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 3 of the License, or
+// (at your option) any later version.
+//
+// ProtonMail Bridge 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 ProtonMail Bridge. If not, see .
+
+package sum
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestRecursiveSum(t *testing.T) {
+ tempDir, err := ioutil.TempDir("", "verify-test")
+ require.NoError(t, err)
+
+ createFiles(t, tempDir,
+ filepath.Join("a", "1"),
+ filepath.Join("a", "2"),
+ filepath.Join("b", "3"),
+ filepath.Join("b", "4"),
+ filepath.Join("b", "c", "5"),
+ filepath.Join("b", "c", "6"),
+ )
+
+ sumOriginal := sum(t, tempDir)
+
+ // Renaming files should produce a different checksum.
+ require.NoError(t, os.Rename(filepath.Join(tempDir, "a", "1"), filepath.Join(tempDir, "a", "11")))
+ sumRenamed := sum(t, tempDir)
+ require.NotEqual(t, sumOriginal, sumRenamed)
+
+ // Reverting to the original name should produce the same checksum again.
+ require.NoError(t, os.Rename(filepath.Join(tempDir, "a", "11"), filepath.Join(tempDir, "a", "1")))
+ require.Equal(t, sumOriginal, sum(t, tempDir))
+
+ // Moving files should produce a different checksum.
+ require.NoError(t, os.Rename(filepath.Join(tempDir, "a", "1"), filepath.Join(tempDir, "1")))
+ sumMoved := sum(t, tempDir)
+ require.NotEqual(t, sumOriginal, sumMoved)
+
+ // Moving files back to their original location should produce the same checksum again.
+ require.NoError(t, os.Rename(filepath.Join(tempDir, "1"), filepath.Join(tempDir, "a", "1")))
+ require.Equal(t, sumOriginal, sum(t, tempDir))
+
+ // Changing file data should produce a different checksum.
+ originalData := modifyFile(t, filepath.Join(tempDir, "a", "1"), []byte("something"))
+ require.NotEqual(t, sumOriginal, sum(t, tempDir))
+
+ // Reverting file data should produce the original checksum.
+ modifyFile(t, filepath.Join(tempDir, "a", "1"), originalData)
+ require.Equal(t, sumOriginal, sum(t, tempDir))
+}
+
+func createFiles(t *testing.T, root string, paths ...string) {
+ for _, path := range paths {
+ makeFile(t, filepath.Join(root, path))
+ }
+}
+
+func makeFile(t *testing.T, path string) {
+ require.NoError(t, os.MkdirAll(filepath.Dir(path), 0700))
+
+ f, err := os.Create(path)
+ require.NoError(t, err)
+
+ _, err = f.WriteString(path)
+ require.NoError(t, err)
+
+ require.NoError(t, f.Close())
+}
+
+func sum(t *testing.T, path string) []byte {
+ sum, err := RecursiveSum(path, "")
+ require.NoError(t, err)
+
+ return sum
+}
+
+func modifyFile(t *testing.T, path string, data []byte) []byte {
+ r, err := os.Open(path)
+ require.NoError(t, err)
+
+ b, err := ioutil.ReadAll(r)
+ require.NoError(t, err)
+ require.NoError(t, r.Close())
+
+ f, err := os.Create(path)
+ require.NoError(t, err)
+
+ _, err = f.Write(data)
+ require.NoError(t, err)
+ require.NoError(t, f.Close())
+
+ return b
+}
diff --git a/utils/hasher/main.go b/utils/hasher/main.go
new file mode 100644
index 00000000..96b590e9
--- /dev/null
+++ b/utils/hasher/main.go
@@ -0,0 +1,62 @@
+// Copyright (c) 2020 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail Bridge 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 3 of the License, or
+// (at your option) any later version.
+//
+// ProtonMail Bridge 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 ProtonMail Bridge. If not, see .
+
+package main
+
+import (
+ "os"
+
+ "github.com/ProtonMail/proton-bridge/pkg/sum"
+ "github.com/sirupsen/logrus"
+ "github.com/urfave/cli/v2"
+)
+
+func main() {
+ if err := createApp().Run(os.Args); err != nil {
+ logrus.Fatal(err)
+ }
+}
+
+func createApp() *cli.App { // nolint[funlen]
+ app := cli.NewApp()
+
+ app.Name = "hasher"
+ app.Usage = "Generate the recursive hash of a directory"
+ app.Action = computeSum
+ app.Flags = []cli.Flag{
+ &cli.StringFlag{
+ Name: "root",
+ Usage: "The root directory from which to begin recursive hashing",
+ Required: true,
+ },
+ }
+
+ return app
+}
+
+func computeSum(c *cli.Context) error {
+ b, err := sum.RecursiveSum(c.String("root"), "")
+ if err != nil {
+ return err
+ }
+
+ if _, err := c.App.Writer.Write(b); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/cmd/versioner/main.go b/utils/versioner/main.go
similarity index 100%
rename from cmd/versioner/main.go
rename to utils/versioner/main.go