Skip to content
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

fetchurl: store files as hex #930

Merged
merged 1 commit into from Apr 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.