197 lines
5.2 KiB
Go
197 lines
5.2 KiB
Go
package stream
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/rand"
|
|
"crypto/sha512"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"path"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const streamTypeLBRYFile = "lbryfile"
|
|
const defaultSanitizedFilename = "lbry_download"
|
|
|
|
// BlobInfo is the stream descriptor info for a single blob in a stream
|
|
// Encoding to and from JSON is customized to match existing behavior (see json.go in package)
|
|
type BlobInfo struct {
|
|
Length int `json:"length"`
|
|
BlobNum int `json:"blob_num"`
|
|
BlobHash []byte `json:"-"`
|
|
IV []byte `json:"-"`
|
|
}
|
|
|
|
// Hash returns the hash of the blob info for calculating the stream hash
|
|
func (bi BlobInfo) Hash() []byte {
|
|
sum := sha512.New384()
|
|
if bi.Length > 0 {
|
|
sum.Write([]byte(hex.EncodeToString(bi.BlobHash)))
|
|
}
|
|
sum.Write([]byte(strconv.Itoa(bi.BlobNum)))
|
|
sum.Write([]byte(hex.EncodeToString(bi.IV)))
|
|
sum.Write([]byte(strconv.Itoa(bi.Length)))
|
|
return sum.Sum(nil)
|
|
}
|
|
|
|
// SDBlob contains information about the rest of the blobs in the stream
|
|
// NOTE: Encoding to and from JSON is customized to match existing behavior (see json.go in package)
|
|
type SDBlob struct {
|
|
StreamName string `json:"-"` // shadowed by JSONSDBlob in json.go
|
|
BlobInfos []BlobInfo `json:"blobs"`
|
|
StreamType string `json:"stream_type"`
|
|
Key []byte `json:"-"` // shadowed by JSONSDBlob in json.go
|
|
SuggestedFileName string `json:"-"` // shadowed by JSONSDBlob in json.go
|
|
StreamHash []byte `json:"-"` // shadowed by JSONSDBlob in json.go
|
|
}
|
|
|
|
// Hash returns a hash of the SD blob data
|
|
func (s SDBlob) Hash() []byte {
|
|
hashBytes := sha512.Sum384(s.ToBlob())
|
|
return hashBytes[:]
|
|
}
|
|
|
|
// HashHex returns the SD blob hash as a hex string
|
|
func (s SDBlob) HashHex() string {
|
|
return hex.EncodeToString(s.Hash())
|
|
}
|
|
|
|
// ToJson returns the SD blob hash as JSON
|
|
func (s SDBlob) ToJson() string {
|
|
j, err := json.MarshalIndent(s, "", " ")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return string(j)
|
|
}
|
|
|
|
// ToBlob converts the SDBlob to a normal data Blob
|
|
func (s SDBlob) ToBlob() Blob {
|
|
jsonSD, err := json.Marshal(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// COMPATIBILITY HACK to make json output match python's json. this can be
|
|
// removed when we implement canonical JSON encoding
|
|
jsonSD = []byte(strings.Replace(string(jsonSD), ",", ", ", -1))
|
|
jsonSD = []byte(strings.Replace(string(jsonSD), ":", ": ", -1))
|
|
|
|
return jsonSD
|
|
}
|
|
|
|
// FromBlob unmarshals a data Blob that should contain SDBlob data
|
|
func (s *SDBlob) FromBlob(b Blob) error {
|
|
return json.Unmarshal(b, s)
|
|
}
|
|
|
|
// addBlob adds the blob's info to stream
|
|
func (s *SDBlob) addBlob(b Blob, iv []byte) {
|
|
if len(iv) == 0 {
|
|
panic("empty IV")
|
|
}
|
|
s.BlobInfos = append(s.BlobInfos, BlobInfo{
|
|
BlobNum: len(s.BlobInfos),
|
|
Length: b.Size(),
|
|
BlobHash: b.Hash(),
|
|
IV: iv,
|
|
})
|
|
}
|
|
|
|
// IsValid returns true if the set StreamHash matches the current hash of the stream data
|
|
func (s SDBlob) IsValid() bool {
|
|
return bytes.Equal(s.StreamHash, s.computeStreamHash())
|
|
}
|
|
|
|
// updateStreamHash sets the stream hash to the current hash of the stream data
|
|
func (s *SDBlob) updateStreamHash() {
|
|
s.StreamHash = s.computeStreamHash()
|
|
}
|
|
|
|
// computeStreamHash calculates the stream hash for the stream
|
|
func (s *SDBlob) computeStreamHash() []byte {
|
|
return streamHash(
|
|
hex.EncodeToString([]byte(s.StreamName)),
|
|
hex.EncodeToString(s.Key),
|
|
hex.EncodeToString([]byte(s.SuggestedFileName)),
|
|
s.BlobInfos,
|
|
)
|
|
}
|
|
|
|
func (s SDBlob) fileSize() int {
|
|
size := 0
|
|
for _, bi := range s.BlobInfos {
|
|
size += bi.Length
|
|
}
|
|
return size
|
|
}
|
|
|
|
// streamHash calculates the stream hash, given the stream's fields and blobs
|
|
func streamHash(hexStreamName, hexKey, hexSuggestedFileName string, blobInfos []BlobInfo) []byte {
|
|
blobSum := sha512.New384()
|
|
for _, b := range blobInfos {
|
|
blobSum.Write(b.Hash())
|
|
}
|
|
|
|
sum := sha512.New384()
|
|
sum.Write([]byte(hexStreamName))
|
|
sum.Write([]byte(hexKey))
|
|
sum.Write([]byte(hexSuggestedFileName))
|
|
sum.Write(blobSum.Sum(nil))
|
|
return sum.Sum(nil)
|
|
}
|
|
|
|
// randIV returns a random AES IV
|
|
func randIV() []byte {
|
|
iv := make([]byte, aes.BlockSize)
|
|
_, err := rand.Read(iv)
|
|
if err != nil {
|
|
panic("failed to make random iv")
|
|
}
|
|
return iv
|
|
}
|
|
|
|
// NullIV returns an IV of 0s
|
|
func NullIV() []byte {
|
|
return make([]byte, aes.BlockSize)
|
|
}
|
|
|
|
var illegalFilenameChars = regexp.MustCompile(`(` +
|
|
`[<>:"/\\|?*]+|` + // Illegal characters
|
|
`[\x00-\x1F]+|` + // All characters in range 0-31
|
|
`[ \t]*(\.)+[ \t]*$|` + // Dots at the end
|
|
`(^[ \t]+|[ \t]+$)|` + // Leading and trailing whitespace
|
|
`^CON$|^PRN$|^AUX$|` + // Illegal names on windows
|
|
`^NUL$|^COM[1-9]$|^LPT[1-9]$` + // Illegal names on windows
|
|
`)`)
|
|
|
|
// sanitizeFilename cleans a filename so it can go into an sd blob
|
|
// python implementation: https://github.com/lbryio/lbry-sdk/blob/e89acac235f497b0215991d5142aa678d525eb59/lbry/stream/descriptor.py#L69
|
|
func sanitizeFilename(name string) string {
|
|
//defaultFilename := "lbry_download"
|
|
|
|
ext := path.Ext(name)
|
|
name = name[:len(name)-len(ext)]
|
|
|
|
if name == "" && ext != "" {
|
|
// python does it this way. I think it's weird, but we should try and match them
|
|
name = ext
|
|
ext = ""
|
|
}
|
|
|
|
name = illegalFilenameChars.ReplaceAllString(name, "")
|
|
ext = illegalFilenameChars.ReplaceAllString(ext, "")
|
|
|
|
if name == "" {
|
|
name = defaultSanitizedFilename
|
|
}
|
|
|
|
if len(ext) > 1 {
|
|
name += ext
|
|
}
|
|
|
|
return name
|
|
}
|