Skip to content

Commit

Permalink
fetchurl: store files as hex
Browse files Browse the repository at this point in the history
Avoid issues with weird characters like + / and = on the filesystem.

The SRI package has been re-structured and is now under pkg/sri
  • Loading branch information
zimbatm committed Dec 31, 2021
1 parent 001472b commit 08b802a
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 97 deletions.
18 changes: 9 additions & 9 deletions internal/cmd/cmd_fetchurl.go
Expand Up @@ -9,7 +9,7 @@ import (
"path/filepath"
"strings"

"github.com/direnv/direnv/v2/sri"
"github.com/direnv/direnv/v2/pkg/sri"
"github.com/mattn/go-isatty"
)

Expand Down Expand Up @@ -40,13 +40,13 @@ func cmdFetchURL(env Env, args []string, config *Config) (err error) {
// Support Base64 where '/' have been replaced by '_'
integrityHash = strings.ReplaceAll(args[2], "_", "/")

algo, err = sri.GetAlgo(integrityHash)
hash, err := sri.Parse(integrityHash)
if err != nil {
return err
}

// Shortcut if the cache already has the file
casFile := casPath(casDir, integrityHash)
casFile := casPath(casDir, hash)
if fileExists(casFile) {
fmt.Println(casFile)
return nil
Expand Down Expand Up @@ -91,8 +91,8 @@ func cmdFetchURL(env Env, args []string, config *Config) (err error) {
}

// Validate if a comparison hash was given
if integrityHash != "" && calculatedHash != integrityHash {
return fmt.Errorf("hash mismatch. Expected '%s' but got '%s'", integrityHash, calculatedHash)
if integrityHash != "" && calculatedHash.String() != integrityHash {
return fmt.Errorf("hash mismatch. Expected '%s' but got '%s'", integrityHash, calculatedHash.String())
}

// Derive the CAS file location from the SRI hash
Expand All @@ -115,7 +115,7 @@ Invoke fetchurl again with the hash as an argument to get the disk location:
direnv fetchurl "%s" "%s"
#=> %s
`, calculatedHash, url, calculatedHash, casFile)
`, calculatedHash, url, calculatedHash.String(), casFile)
} else {
// Only print the hash in scripting mode. Add one extra hurdle on
// purpose to use fetchurl without the SRI hash.
Expand All @@ -133,8 +133,8 @@ func casDir(c *Config) string {
}

// casPath returns filesystem path for SRI hashes
func casPath(dir string, integrityHash string) string {
// avoid / in the filename
sriFile := strings.ReplaceAll(integrityHash, "/", "_")
func casPath(dir string, integrityHash *sri.Hash) string {
// Use Hex encoding for the filesystem to avoid issues
sriFile := integrityHash.Hex()
return filepath.Join(dir, sriFile)
}
37 changes: 37 additions & 0 deletions pkg/sri/parse.go
@@ -0,0 +1,37 @@
package sri

import (
"fmt"
"strings"
)

// Parse a SRI hash
func Parse(sriHash string) (*Hash, error) {
elems := strings.SplitN(sriHash, "-", 2)
if len(elems) != 2 {
return nil, fmt.Errorf("sri: not a hash %v", sriHash)
}

// Get the algo
var algo Algo
switch elems[0] {
case string(SHA256):
algo = SHA256
case string(SHA384):
algo = SHA384
case string(SHA512):
algo = SHA512
default:
return nil, fmt.Errorf("sri: unsupported algo %s", elems[0])
}

// Get the hash
dbuf := make([]byte, b64Enc.DecodedLen(len(elems[1])))
n, err := b64Enc.Decode(dbuf, []byte(elems[1]))
if err != nil {
return nil, err
}
sum := dbuf[:n]

return &Hash{string(algo), sum}, nil
}
40 changes: 40 additions & 0 deletions pkg/sri/sri.go
@@ -0,0 +1,40 @@
// Package sri implements helper functions to calculate SubResource Integrity
// hashes.
// https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
package sri

import (
b64 "encoding/base64"
"encoding/hex"
)

// Algo is a supported hashing algorithm
type Algo string

const (
// SHA256 algo
SHA256 = Algo("sha256")
// SHA384 algo
SHA384 = Algo("sha384")
// SHA512 algo
SHA512 = Algo("sha512")
)

// Base64 encoding to use
var b64Enc = b64.StdEncoding

// Hash represents a SRI-hash
type Hash struct {
algo string
sum []byte
}

// String returns a SRI-encoded string
func (h *Hash) String() string {
return h.algo + "-" + b64Enc.EncodeToString(h.sum)
}

// Hex return a hex-encoded representation of the sum
func (h *Hash) Hex() string {
return hex.EncodeToString(h.sum)
}
17 changes: 15 additions & 2 deletions sri/sri_test.go → pkg/sri/sri_test.go
Expand Up @@ -5,7 +5,7 @@ import (
"testing"
)

func TestSRIHasher(t *testing.T) {
func TestWriter(t *testing.T) {
var b strings.Builder

s := "testdata"
Expand All @@ -29,8 +29,21 @@ func TestSRIHasher(t *testing.T) {
}

// Check that the hash has been calculated properly
x := w.Sum()
x := w.Sum().String()
if x != expectedHash {
t.Fatal("hash mismatch")
}
}

func TestParser(t *testing.T) {
expectedHash := "sha256-gQ/y+yQqXe5CIPLLDmpRmJH7Z/L4KKbKtO+IlGM7H1A="

hash, err := Parse(expectedHash)
if err != nil {
t.Fatalf("parse error: %v", err)
}

if hash.String() != expectedHash {
t.Fatal("hash mismatch")
}
}
49 changes: 49 additions & 0 deletions pkg/sri/writer.go
@@ -0,0 +1,49 @@
package sri

import (
"crypto/sha256"
"crypto/sha512"
"hash"
"io"
)

// Writer is like a hash.Hash with a Sum function
type Writer struct {
w io.Writer
algo Algo
h hash.Hash
}

// NewWriter returns a SRI writer that forwards the write while calculating
// the SRI hash.
func NewWriter(w io.Writer, algo Algo) Writer {
var h hash.Hash
switch algo {
case SHA256:
h = sha256.New()
case SHA384:
h = sha512.New384()
case SHA512:
h = sha512.New()
default:
panic("unsupported SRI algo")
}

return Writer{w, algo, h}
}

func (w Writer) Write(b []byte) (int, error) {
// First write to the underlying storage
n, err := w.w.Write(b)
if err == nil {
// This should always succeed
_, _ = w.h.Write(b)
}
return n, err
}

// Sum returns the calculated SRI hash
func (w Writer) Sum() *Hash {
sum := w.h.Sum(nil)
return &Hash{string(w.algo), sum}
}
86 changes: 0 additions & 86 deletions sri/sri.go

This file was deleted.

0 comments on commit 08b802a

Please sign in to comment.