Repository: Eyevinn/mp4ff
Pull request #392 (2024-12-13 14:10:22)

Encrypt tools take base64 in addition to hex key and keyID values


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4913a02..2b5017d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
-- Nothing yet
+### Changed
+
+- mp4.NewUUIDFromHex() changed to more general mp4.NewUUIDFromString()
+- cmd/mp4ff-decrypt -key option instead of -k. Takes hex or base64 value
+- cmd/mp4ff-encrypt -key and -kid options now take hex or bae64 values
+
+### Added
+
+- mp4.SetUUID() can take base64 string as well as hex-encoded.
 
 ## [0.47.0] - 2024-11-12
 
diff --git a/cmd/mp4ff-decrypt/doc.go b/cmd/mp4ff-decrypt/doc.go
index 91a1b60..3eb5035 100644
--- a/cmd/mp4ff-decrypt/doc.go
+++ b/cmd/mp4ff-decrypt/doc.go
@@ -2,17 +2,16 @@
 mp4ff-decrypt decrypts a fragmented mp4 file encrypted with Common Encryption scheme cenc or cbcs.
 For a media segment, it needs an init segment with encryption information.
 
-	    Usage of mp4ff-decrypt:
+Usage of mp4ff-decrypt:
+mp4ff-decrypt [options] infile outfile
 
-	        mp4ff-decrypt [options] infile outfile
+options:
 
-	    options:
-
-		    -init string
-		  	    Path to init file with encryption info (scheme, kid, pssh)
-		    -k string
-		     	Required: key (hex)
-		    -version
-		  	    Get mp4ff version
+	-init string
+	  	Path to init file with encryption info (scheme, kid, pssh)
+	-key string
+	  	Required: key (32 hex or 24 base64 chars)
+	-version
+	  	Get mp4ff version
 */
 package main
diff --git a/cmd/mp4ff-decrypt/main.go b/cmd/mp4ff-decrypt/main.go
index 49af9d8..3c0ac90 100644
--- a/cmd/mp4ff-decrypt/main.go
+++ b/cmd/mp4ff-decrypt/main.go
@@ -1,7 +1,6 @@
 package main
 
 import (
-	"encoding/hex"
 	"errors"
 	"flag"
 	"fmt"
@@ -24,7 +23,7 @@ Usage of %s:
 
 type options struct {
 	initFilePath string
-	hexKey       string
+	keyStr       string
 	version      bool
 }
 
@@ -37,7 +36,7 @@ func parseOptions(fs *flag.FlagSet, args []string) (*options, error) {
 
 	opts := options{}
 	fs.StringVar(&opts.initFilePath, "init", "", "Path to init file with encryption info (scheme, kid, pssh)")
-	fs.StringVar(&opts.hexKey, "k", "", "Required: key (hex)")
+	fs.StringVar(&opts.keyStr, "key", "", "Required: key (32 hex or 24 base64 chars)")
 	fs.BoolVar(&opts.version, "version", false, "Get mp4ff version")
 	err := fs.Parse(args[1:])
 	return &opts, err
@@ -74,8 +73,13 @@ func run(args []string) error {
 	var inFilePath = fs.Arg(0)
 	var outFilePath = fs.Arg(1)
 
-	if opts.hexKey == "" {
-		return fmt.Errorf("no hex key specified")
+	if opts.keyStr == "" {
+		return fmt.Errorf("no key specified")
+	}
+
+	key, err := mp4.UnpackKey(opts.keyStr)
+	if err != nil {
+		return fmt.Errorf("unpacking key: %w", err)
 	}
 
 	ifh, err := os.Open(inFilePath)
@@ -96,22 +100,15 @@ func run(args []string) error {
 		}
 		defer inith.Close()
 	}
-	err = decryptFile(ifh, inith, ofh, opts.hexKey)
+
+	err = decryptFile(ifh, inith, ofh, key)
 	if err != nil {
 		return fmt.Errorf("decryptFile: %w", err)
 	}
 	return nil
 }
 
-func decryptFile(r, initR io.Reader, w io.Writer, hexKey string) error {
-
-	if len(hexKey) != 32 {
-		return fmt.Errorf("hex key must have length 32 chars")
-	}
-	key, err := hex.DecodeString(hexKey)
-	if err != nil {
-		return err
-	}
+func decryptFile(r, initR io.Reader, w io.Writer, key []byte) error {
 	inMp4, err := mp4.DecodeFile(r)
 	if err != nil {
 		return err
diff --git a/cmd/mp4ff-encrypt/doc.go b/cmd/mp4ff-encrypt/doc.go
index 87e4b1b..0741729 100644
--- a/cmd/mp4ff-encrypt/doc.go
+++ b/cmd/mp4ff-encrypt/doc.go
@@ -1,27 +1,27 @@
 /*
 mp4ff-encrypt encrypts a fragmented mp4 file using Common Encryption with cenc or cbcs scheme.
 A combined fragmented file with init segment and media segment(s) will be encrypted.
-For a pure media segment, an init segment with encryption information is needed
+For a pure media segment, an init segment with encryption information is needed.
 
-	Usage of mp4ff-encrypt:
+Usage of mp4ff-encrypt:
 
-	    mp4ff-encrypt [options] infile outfile
+mp4ff-encrypt [options] infile outfile
 
-	options:
+options:
 
-		-init string
-		      Path to init file with encryption info (scheme, kid, pssh)
-		-iv string
-		      Required: iv (16 or 32 hex chars)
-		-key string
-		      Required: key (32 hex chars)
-		-kid string
-		      key id (32 hex chars). Required if initFilePath empty
-		-pssh string
-		      file with one or more pssh box(es) in binary format. Will be added at end of moov box
-		-scheme string
-		      cenc or cbcs. Required if initFilePath empty (default "cenc")
-		-version
-		      Get mp4ff version
+	-init string
+	  	Path to init file with encryption info (scheme, kid, pssh)
+	-iv string
+	  	Required: iv (16 or 32 hex chars)
+	-key string
+	  	Required: key (32 hex or 24 base64 chars)
+	-kid string
+	  	key id (32 hex or 24 base64 chars). Required if initFilePath empty
+	-pssh string
+	  	file with one or more pssh box(es) in binary format. Will be added at end of moov box
+	-scheme string
+	  	cenc or cbcs. Required if initFilePath empty (default "cenc")
+	-version
+	  	Get mp4ff version
 */
 package main
diff --git a/cmd/mp4ff-encrypt/main.go b/cmd/mp4ff-encrypt/main.go
index 01578b6..68e55ea 100644
--- a/cmd/mp4ff-encrypt/main.go
+++ b/cmd/mp4ff-encrypt/main.go
@@ -25,8 +25,8 @@ Usage of %s:
 
 type options struct {
 	initFile string
-	kidHex   string
-	keyHex   string
+	kidStr   string
+	keyStr   string
 	ivHex    string
 	scheme   string
 	psshFile string
@@ -43,8 +43,8 @@ func parseOptions(fs *flag.FlagSet, args []string) (*options, error) {
 	opts := options{}
 
 	fs.StringVar(&opts.initFile, "init", "", "Path to init file with encryption info (scheme, kid, pssh)")
-	fs.StringVar(&opts.kidHex, "kid", "", "key id (32 hex chars). Required if initFilePath empty")
-	fs.StringVar(&opts.keyHex, "key", "", "Required: key (32 hex chars)")
+	fs.StringVar(&opts.kidStr, "kid", "", "key id (32 hex or 24 base64 chars). Required if initFilePath empty")
+	fs.StringVar(&opts.keyStr, "key", "", "Required: key (32 hex or 24 base64 chars)")
 	fs.StringVar(&opts.ivHex, "iv", "", "Required: iv (16 or 32 hex chars)")
 	fs.StringVar(&opts.scheme, "scheme", "cenc", "cenc or cbcs. Required if initFilePath empty")
 	fs.StringVar(&opts.psshFile, "pssh", "", "file with one or more pssh box(es) in binary format. Will be added at end of moov box")
@@ -85,7 +85,7 @@ func run(args []string) error {
 	var inFilePath = fs.Arg(0)
 	var outFilePath = fs.Arg(1)
 
-	if opts.keyHex == "" || opts.ivHex == "" {
+	if opts.keyStr == "" || opts.ivHex == "" {
 		fs.Usage()
 		return fmt.Errorf("need both key and iv")
 	}
@@ -127,7 +127,7 @@ func run(args []string) error {
 		}
 	}
 
-	err = encryptFile(ifh, ofh, initSeg, opts.scheme, opts.kidHex, opts.keyHex, opts.ivHex, psshData)
+	err = encryptFile(ifh, ofh, initSeg, opts.scheme, opts.kidStr, opts.keyStr, opts.ivHex, psshData)
 	if err != nil {
 		return fmt.Errorf("encryptFile: %w", err)
 	}
@@ -135,7 +135,7 @@ func run(args []string) error {
 }
 
 func encryptFile(ifh io.Reader, ofh io.Writer, initSeg *mp4.InitSegment,
-	scheme, kidHex, keyHex, ivHex string, psshData []byte) error {
+	scheme, kidStr, keyStr, ivHex string, psshData []byte) error {
 
 	if len(ivHex) != 32 && len(ivHex) != 16 {
 		return fmt.Errorf("hex iv must have length 16 or 32 chars; %d", len(ivHex))
@@ -145,23 +145,22 @@ func encryptFile(ifh io.Reader, ofh io.Writer, initSeg *mp4.InitSegment,
 		return fmt.Errorf("invalid iv %s", ivHex)
 	}
 
-	if len(keyHex) != 32 {
-		return fmt.Errorf("hex key must have length 32 chars: %d", len(keyHex))
+	if len(keyStr) != 32 {
+		return fmt.Errorf("hex key must have length 32 chars: %d", len(keyStr))
 	}
-	key, err := hex.DecodeString(keyHex)
+	key, err := mp4.UnpackKey(keyStr)
 	if err != nil {
-		return fmt.Errorf("invalid key %s", keyHex)
+		return fmt.Errorf("invalid key %s, %w", keyStr, err)
 	}
 
 	var kidUUID mp4.UUID
 	if initSeg == nil {
-		if len(kidHex) != 32 {
-			return fmt.Errorf("hex key id must have length 32 chars: %d", len(kidHex))
-		}
-		kidUUID, err = mp4.NewUUIDFromHex(kidHex)
+		kid, err := mp4.UnpackKey(kidStr)
 		if err != nil {
-			return fmt.Errorf("invalid kid %s", kidHex)
+			return fmt.Errorf("invalid key ID %s: %w", kidStr, err)
 		}
+		kidHex := hex.EncodeToString(kid)
+		kidUUID, _ = mp4.NewUUIDFromString(kidHex)
 		if scheme != "cenc" && scheme != "cbcs" {
 			return fmt.Errorf("scheme must be cenc or cbcs: %s", scheme)
 		}
diff --git a/mp4/pssh.go b/mp4/pssh.go
index 04dd46a..8cffb5e 100644
--- a/mp4/pssh.go
+++ b/mp4/pssh.go
@@ -19,32 +19,6 @@ const (
 	UUID_W3C_COMMON = "1077efec-c0b2-4d02-ace3-3c1e52e2fb4b"
 )
 
-// UUID - 16-byte KeyID or SystemID
-type UUID []byte
-
-func (u UUID) String() string {
-	h := hex.EncodeToString(u)
-	if len(u) != 16 {
-		return h
-	}
-	return fmt.Sprintf("%s-%s-%s-%s-%s", h[0:8], h[8:12], h[12:16], h[16:20], h[20:32])
-}
-
-// NewUUIDFromHex creates a UUID from a hexadecimal string with 32 chars or 36 chars (with dashes)
-func NewUUIDFromHex(h string) (UUID, error) {
-	if len(h) == 36 {
-		h = strings.ReplaceAll(h, "-", "")
-	}
-	if len(h) != 32 {
-		return nil, fmt.Errorf("hex has %d chars, not 32", len(h))
-	}
-	s, err := hex.DecodeString(h)
-	if err != nil {
-		return nil, err
-	}
-	return UUID(s), nil
-}
-
 // ProtectionSystemName returns name of protection system if known.
 func ProtectionSystemName(systemID UUID) string {
 	uStr := systemID.String()
diff --git a/mp4/uuid.go b/mp4/uuid.go
index 56b5612..4fa6c02 100644
--- a/mp4/uuid.go
+++ b/mp4/uuid.go
@@ -2,6 +2,7 @@ package mp4
 
 import (
 	"bytes"
+	"encoding/base64"
 	"encoding/hex"
 	"fmt"
 	"io"
@@ -10,6 +11,27 @@ import (
 	"github.com/Eyevinn/mp4ff/bits"
 )
 
+// UUID - 16-byte KeyID or SystemID
+type UUID []byte
+
+func (u UUID) String() string {
+	if len(u) != 16 {
+		return fmt.Sprintf("bad uuid %q", hex.EncodeToString(u))
+	}
+	h := hex.EncodeToString(u[:])
+	return fmt.Sprintf("%s-%s-%s-%s-%s", h[0:8], h[8:12], h[12:16], h[16:20], h[20:32])
+}
+
+// Equal compares with other UUID
+func (u UUID) Equal(a UUID) bool {
+	return bytes.Equal(u[:], a[:])
+}
+
+// NewUUIDFromString creates a UUID from a hexadecimal, uuid-string or base64 string
+func NewUUIDFromString(h string) (UUID, error) {
+	return createUUID(h)
+}
+
 const (
 	// The following UUIDs belong to Microsoft Smooth Streaming Protocol (MSS)
 
@@ -29,34 +51,17 @@ const (
 	UUIDPiffSenc = "a2394f52-5a9b-4f14-a244-6c427c648df4"
 )
 
-// uuid - compact representation of UUID
-type uuid [16]byte
-
-// String - UUID-formatted string
-func (u uuid) String() string {
-	hexStr := hex.EncodeToString(u[:])
-	return fmt.Sprintf("%s-%s-%s-%s-%s", hexStr[:8], hexStr[8:12], hexStr[12:16], hexStr[16:20], hexStr[20:])
-}
-
-// Equal - compare with other uuid
-func (u uuid) Equal(a uuid) bool {
-	return bytes.Equal(u[:], a[:])
-}
-
-// createUUID - create uuid from string
-func createUUID(u string) (uuid, error) {
-	var a uuid
-	stripped := strings.ReplaceAll(u, "-", "")
-	b, err := hex.DecodeString(stripped)
-	if err != nil || len(b) != 16 {
-		return a, fmt.Errorf("bad uuid string: %s", u)
+// createUUID - create uuid from hex, uuid-formatted hex, or base64 string
+func createUUID(u string) (UUID, error) {
+	b, err := UnpackKey(u)
+	if err != nil {
+		return nil, err
 	}
-	_ = copy(a[:], b)
-	return a, nil
+	return UUID(b), nil
 }
 
 // mustCreateUUID - create uuid from string. Panic for bad string
-func mustCreateUUID(u string) uuid {
+func mustCreateUUID(u string) UUID {
 	b, err := createUUID(u)
 	if err != nil {
 		panic(err.Error())
@@ -65,15 +70,15 @@ func mustCreateUUID(u string) uuid {
 }
 
 var (
-	uuidTfxd     uuid = mustCreateUUID(UUIDTfxd)
-	uuidTfrf     uuid = mustCreateUUID(UUIDTfrf)
-	uuidPiffSenc uuid = mustCreateUUID(UUIDPiffSenc)
+	uuidTfxd     UUID = mustCreateUUID(UUIDTfxd)
+	uuidTfrf     UUID = mustCreateUUID(UUIDTfrf)
+	uuidPiffSenc UUID = mustCreateUUID(UUIDPiffSenc)
 )
 
 // UUIDBox - Used as container for MSS boxes tfxd and tfrf
 // For unknown UUID, the data after the UUID is stored as UnknownPayload
 type UUIDBox struct {
-	uuid           uuid
+	uuid           UUID
 	Tfxd           *TfxdData
 	Tfrf           *TfrfData
 	Senc           *SencBox
@@ -86,7 +91,8 @@ func (u *UUIDBox) UUID() string {
 	return u.uuid.String()
 }
 
-// UUID - Set UUID from string
+// UUID - Set UUID from string corresponding to 16 bytes.
+// The input should be a UUID-formatted hex string, plain hex or baset64 encoded.
 func (u *UUIDBox) SetUUID(uuid string) (err error) {
 	u.uuid, err = createUUID(uuid)
 	return err
@@ -123,8 +129,10 @@ func DecodeUUIDBox(hdr BoxHeader, startPos uint64, r io.Reader) (Box, error) {
 
 // DecodeUUIDBoxSR - decode a UUID box including tfxd or tfrf
 func DecodeUUIDBoxSR(hdr BoxHeader, startPos uint64, sr bits.SliceReader) (Box, error) {
-	b := &UUIDBox{StartPos: startPos}
-	copy(b.uuid[:], sr.ReadBytes(16))
+	b := &UUIDBox{
+		StartPos: startPos,
+		uuid:     sr.ReadBytes(16),
+	}
 	switch b.UUID() {
 	case UUIDTfxd:
 		tfxd, err := decodeTfxd(sr)
@@ -330,3 +338,38 @@ func (b *UUIDBox) Info(w io.Writer, specificBoxLevels, indent, indentStep string
 	}
 	return bd.err
 }
+
+// UnpackKey unpacks a hex or base64 encoded 16-byte key.
+// The key can be in uuid formats with hyphens at positions 8, 13, 18, 23.
+func UnpackKey(inKey string) (key []byte, err error) {
+	shorten := func(s string) string {
+		return fmt.Sprintf("%s...%s", s[:6], s[len(s)-6:])
+	}
+	switch len(inKey) {
+	case 36:
+		if inKey[8] != '-' || inKey[13] != '-' || inKey[18] != '-' || inKey[23] != '-' {
+			return nil, fmt.Errorf("bad uuid format: %s", shorten(inKey))
+		}
+		inKey = strings.ReplaceAll(inKey, "-", "")
+		if len(inKey) != 32 {
+			return nil, fmt.Errorf("bad uuid format: %s", shorten(inKey))
+		}
+		key, err = hex.DecodeString(inKey)
+		if err != nil {
+			return nil, fmt.Errorf("bad uuid %s: %w", shorten(inKey), err)
+		}
+	case 32:
+		key, err = hex.DecodeString(inKey)
+		if err != nil {
+			return nil, fmt.Errorf("bad hex %s: %w", shorten(inKey), err)
+		}
+	case 24:
+		key, err = base64.StdEncoding.DecodeString(inKey)
+		if err != nil {
+			return nil, fmt.Errorf("bad base64 %s: %w", shorten(inKey), err)
+		}
+	default:
+		return nil, fmt.Errorf("cannot decode key %s", inKey)
+	}
+	return key, nil
+}


Test patch:
diff --git a/cmd/mp4ff-decrypt/main_test.go b/cmd/mp4ff-decrypt/main_test.go
index ce18955..90b8ef7 100644
--- a/cmd/mp4ff-decrypt/main_test.go
+++ b/cmd/mp4ff-decrypt/main_test.go
@@ -6,6 +6,8 @@ import (
 	"os"
 	"path"
 	"testing"
+
+	"github.com/Eyevinn/mp4ff/mp4"
 )
 
 func TestNonRunningOptionCases(t *testing.T) {
@@ -24,12 +26,12 @@ func TestNonRunningOptionCases(t *testing.T) {
 		{desc: "unknown args", args: []string{"mp4ff-decrypt", "-x"}, err: true},
 		{desc: "no outfile", args: []string{"mp4ff-decrypt", "infile.mp4"}, err: true},
 		{desc: "no key", args: []string{"mp4ff-decrypt", "infile.mp4", outFile}, err: true},
-		{desc: "non-existing infile", args: []string{"mp4ff-decrypt", "-k", key, "infile.mp4", outFile}, err: true},
-		{desc: "non-existing initfile", args: []string{"mp4ff-decrypt", "-init", "init.mp4", "-k", key, infile, outFile}, err: true},
-		{desc: "bad infile", args: []string{"mp4ff-decrypt", "-k", key, "main.go", outFile}, err: true},
-		{desc: "short key", args: []string{"mp4ff-decrypt", "-k", "ab", infile, outFile}, err: true},
-		{desc: "bad key", args: []string{"mp4ff-decrypt", "-k", badKey, infile, outFile}, err: true},
-		{desc: "non-encrypted file", args: []string{"mp4ff-decrypt", "-k", key, nonEncryptedFile, outFile}, err: false},
+		{desc: "non-existing infile", args: []string{"mp4ff-decrypt", "-key", key, "infile.mp4", outFile}, err: true},
+		{desc: "non-existing initfile", args: []string{"mp4ff-decrypt", "-init", "init.mp4", "-key", key, infile, outFile}, err: true},
+		{desc: "bad infile", args: []string{"mp4ff-decrypt", "-key", key, "main.go", outFile}, err: true},
+		{desc: "short key", args: []string{"mp4ff-decrypt", "-key", "ab", infile, outFile}, err: true},
+		{desc: "bad key", args: []string{"mp4ff-decrypt", "-key", badKey, infile, outFile}, err: true},
+		{desc: "non-encrypted file", args: []string{"mp4ff-decrypt", "-key", key, nonEncryptedFile, outFile}, err: false},
 		{desc: "version", args: []string{"mp4ff-decrypt", "-version"}, err: false},
 		{desc: "help", args: []string{"mp4ff-decrypt", "-h"}, err: false},
 	}
@@ -52,38 +54,44 @@ func TestDecodeFiles(t *testing.T) {
 		initFile        string
 		inFile          string
 		expectedOutFile string
-		hexKey          string
+		keyHexOrBase64  string
 	}{
 		{
 			desc:            "cenc",
 			inFile:          "../../mp4/testdata/prog_8s_enc_dashinit.mp4",
 			expectedOutFile: "../../mp4/testdata/prog_8s_dec_dashinit.mp4",
-			hexKey:          "63cb5f7184dd4b689a5c5ff11ee6a328",
+			keyHexOrBase64:  "63cb5f7184dd4b689a5c5ff11ee6a328",
+		},
+		{
+			desc:            "cenc with base64 key",
+			inFile:          "../../mp4/testdata/prog_8s_enc_dashinit.mp4",
+			expectedOutFile: "../../mp4/testdata/prog_8s_dec_dashinit.mp4",
+			keyHexOrBase64:  "Y8tfcYTdS2iaXF/xHuajKA==",
 		},
 		{
 			desc:            "cbcs",
 			inFile:          "../../mp4/testdata/cbcs.mp4",
 			expectedOutFile: "../../mp4/testdata/cbcsdec.mp4",
-			hexKey:          "22bdb0063805260307ee5045c0f3835a",
+			keyHexOrBase64:  "22bdb0063805260307ee5045c0f3835a",
 		},
 		{
 			desc:            "cbcs audio",
 			inFile:          "../../mp4/testdata/cbcs_audio.mp4",
 			expectedOutFile: "../../mp4/testdata/cbcs_audiodec.mp4",
-			hexKey:          "5ffd93861fa776e96cccd934898fc1c8",
+			keyHexOrBase64:  "5ffd93861fa776e96cccd934898fc1c8",
 		},
 		{
 			desc:            "PIFF audio",
 			initFile:        "testdata/PIFF/audio/init.mp4",
 			inFile:          "testdata/PIFF/audio/segment-1.0001.m4s",
 			expectedOutFile: "testdata/PIFF/audio/segment-1.0001_dec.m4s",
-			hexKey:          "602a9289bfb9b1995b75ac63f123fc86",
+			keyHexOrBase64:  "602a9289bfb9b1995b75ac63f123fc86",
 		},
 		{
 			desc:            "PIFF video",
 			inFile:          "testdata/PIFF/video/complseg-1.0001.mp4",
 			expectedOutFile: "testdata/PIFF/video/complseg-1.0001_dec.mp4",
-			hexKey:          "602a9289bfb9b1995b75ac63f123fc86",
+			keyHexOrBase64:  "602a9289bfb9b1995b75ac63f123fc86",
 		},
 	}
 	tmpDir := t.TempDir()
@@ -94,7 +102,7 @@ func TestDecodeFiles(t *testing.T) {
 			if c.initFile != "" {
 				args = append(args, "-init", c.initFile)
 			}
-			args = append(args, "-k", c.hexKey, c.inFile, outFile)
+			args = append(args, "-key", c.keyHexOrBase64, c.inFile, outFile)
 			err := run(args)
 			if err != nil {
 				t.Error(err)
@@ -127,7 +135,11 @@ func BenchmarkDecodeCenc(b *testing.B) {
 	for i := 0; i < b.N; i++ {
 		inBuf := bytes.NewBuffer(raw)
 		outBuf.Reset()
-		err = decryptFile(inBuf, nil, outBuf, hexKey)
+		key, err := mp4.UnpackKey(hexKey)
+		if err != nil {
+			b.Error(err)
+		}
+		err = decryptFile(inBuf, nil, outBuf, key)
 		if err != nil {
 			b.Error(err)
 		}
diff --git a/mp4/crypto_test.go b/mp4/crypto_test.go
index a6a5e5b..20381aa 100644
--- a/mp4/crypto_test.go
+++ b/mp4/crypto_test.go
@@ -105,7 +105,7 @@ func TestEncryptDecrypt(t *testing.T) {
 	ivHex16 := "ffeeddccbbaa99887766554433221100"
 	kidHex := "11112222333344445555666677778888"
 	key, _ := hex.DecodeString(keyHex)
-	kidUUID, _ := NewUUIDFromHex(kidHex)
+	kidUUID, _ := NewUUIDFromString(kidHex)
 	psshFile := "testdata/pssh.bin"
 	psh, err := os.Open(psshFile)
 	if err != nil {
diff --git a/mp4/pssh_test.go b/mp4/pssh_test.go
index 407fe8a..f10cdd2 100644
--- a/mp4/pssh_test.go
+++ b/mp4/pssh_test.go
@@ -33,12 +33,12 @@ func TestPsshFromBase64(t *testing.T) {
 
 func TestEncodeDecodePSSH(t *testing.T) {
 	hPR := strings.ReplaceAll(UUIDPlayReady, "-", "")
-	pr, err := NewUUIDFromHex(hPR)
+	pr, err := NewUUIDFromString(hPR)
 	if err != nil {
 		t.Fatal(err)
 	}
 	kid := "00112233445566778899aabbccddeeff"
-	ku, err := NewUUIDFromHex(kid)
+	ku, err := NewUUIDFromString(kid)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -71,7 +71,7 @@ func TestPsshUUIDs(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		u, err := NewUUIDFromHex(c.hexUUIDs)
+		u, err := NewUUIDFromString(c.hexUUIDs)
 		if err != nil {
 			t.Fatal(err)
 		}
diff --git a/mp4/uuid_test.go b/mp4/uuid_test.go
index 51c4681..823225c 100644
--- a/mp4/uuid_test.go
+++ b/mp4/uuid_test.go
@@ -60,7 +60,7 @@ func TestUUIDVariants(t *testing.T) {
 func TestSetUUID(t *testing.T) {
 	testCases := []struct {
 		uuidStr    string
-		expected   uuid
+		expected   UUID
 		shouldFail bool
 	}{
 		{
@@ -109,3 +109,75 @@ func TestUUIDEncodeDecoder(t *testing.T) {
 	}
 	boxDiffAfterEncodeAndDecode(t, tfxd)
 }
+
+func TestUnpackKey(t *testing.T) {
+	cases := []struct {
+		desc        string
+		keyStr      string
+		expected    []byte
+		expectedErr string
+	}{
+		{
+			desc:   "valid hex key",
+			keyStr: "00112233445566778899aabbccddeeff",
+			expected: []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
+				0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
+			expectedErr: "",
+		},
+		{
+			desc:        "invalid hex key",
+			keyStr:      "0011223x445566778899aabbccddeeff",
+			expectedErr: "bad hex 001122...ddeeff: encoding/hex: invalid byte: U+0078 'x'",
+		},
+		{
+			desc:        "wrong length key",
+			keyStr:      "00112233445566778899aab",
+			expectedErr: "cannot decode key 00112233445566778899aab",
+		},
+		{
+			desc:   "good uuid",
+			keyStr: "00112233-4455-6677-8899-aabbccddeeff",
+			expected: []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
+				0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
+			expectedErr: "",
+		},
+		{
+			desc:        "bad uuid, misplaced dashes",
+			keyStr:      "00----112233445566778899aabbccddeeff",
+			expectedErr: "bad uuid format: 00----...ddeeff",
+		},
+		{
+			desc:        "bad uuid too many dashes",
+			keyStr:      "00112233-4-55-6677-8899-aabbccddeeff",
+			expectedErr: "bad uuid format: 001122...ddeeff",
+		},
+		{
+			desc:        "bad hex in uuid",
+			keyStr:      "0011223x-4455-6677-8899-aabbccddeeff",
+			expectedErr: "bad uuid 001122...ddeeff: encoding/hex: invalid byte: U+0078 'x'",
+		},
+		{
+			desc:        "valid base64 key",
+			keyStr:      "ABEiM0RVZneImaq7zN3u/w=-",
+			expectedErr: "bad base64 ABEiM0...3u/w=-: illegal base64 data at input byte 22",
+		},
+	}
+	for _, c := range cases {
+		t.Run(c.desc, func(t *testing.T) {
+			key, err := UnpackKey(c.keyStr)
+			if c.expectedErr != "" {
+				if err == nil {
+					t.Error("expected error but got nil")
+				}
+				if err.Error() != c.expectedErr {
+					t.Errorf("error %q not matching expected error %q", err, c.expectedErr)
+				}
+				return
+			}
+			if !bytes.Equal(key, c.expected) {
+				t.Errorf("got %x instead of %x", key, c.expected)
+			}
+		})
+	}
+
+}