tweaked object ID format

This commit is contained in:
Jarek Kowalski
2016-05-19 19:56:36 -07:00
parent e5f646e32d
commit 6812f3a9cd
7 changed files with 194 additions and 183 deletions

View File

@@ -18,7 +18,7 @@
createMaxBlobSize = createCommand.Flag("max-blob-size", "Maximum size of a data chunk in bytes.").Default("20000000").Int()
createInlineBlobSize = createCommand.Flag("inline-blob-size", "Maximum size of an inline data chunk in bytes.").Default("32768").Int()
createVaultEncryptionFormat = createCommand.Flag("vault-encryption", "Vault encryption format").Default("aes-256").Enum(supportedVaultEncryptionFormats()...)
createObjectFormat = createCommand.Flag("object-format", "Specifies custom object format to be used").Default("hmac-sha256").Enum(supportedObjectFormats()...)
createObjectFormat = createCommand.Flag("object-format", "Specifies custom object format to be used").Default("sha256t128-aes256").Enum(supportedObjectFormats()...)
createOverwrite = createCommand.Flag("overwrite", "Overwrite existing data.").Bool()
)

View File

@@ -3,11 +3,20 @@
import (
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"hash"
"io"
"golang.org/x/crypto/hkdf"
)
var (
hkdfInfoBlockID = []byte("BlockID")
hkdfInfoCryptoKey = []byte("CryptoKey")
)
// Format describes the format of object data.
@@ -21,46 +30,11 @@ type Format struct {
// ObjectIDFormat describes single format ObjectID
type ObjectIDFormat struct {
Name string
Name string
IsEncrypted bool
hashFuncMaker func(secret []byte) func() hash.Hash
createCipher func([]byte) (cipher.Block, error)
keygen keygenFunc
}
// IsEncrypted determines whether the ObjectIDFormat is encrypted.
func (oif *ObjectIDFormat) IsEncrypted() bool {
return oif.createCipher != nil
}
// HashSizeBits returns the number of bits returned by hash function.
func (oif *ObjectIDFormat) HashSizeBits() int {
hf := oif.hashFuncMaker(nil)
return hf().Size() * 8
}
// BlockIDLength returns the number of characters in a stored block ID.
func (oif *ObjectIDFormat) BlockIDLength() int {
hf := oif.hashFuncMaker(nil)
if oif.keygen == nil {
return hf().Size() * 2
}
h := hf().Sum(nil)
blockID, _ := oif.keygen(h)
return len(blockID) * 2
}
// EncryptionKeySizeBits returns the size of encryption key in bits,
// or zero if no encryption is used in this format.
func (oif *ObjectIDFormat) EncryptionKeySizeBits() int {
if oif.keygen == nil {
return 0
}
hf := oif.hashFuncMaker(nil)
h := hf().Sum(nil)
_, key := oif.keygen(h)
return len(key) * 8
hashBuffer func(data []byte, secret []byte) ([]byte, []byte)
createCipher func(key []byte) (cipher.Block, error)
}
// ObjectIDFormats is a collection of ObjectIDFormat
@@ -77,27 +51,82 @@ func (fmts ObjectIDFormats) Find(name string) *ObjectIDFormat {
return nil
}
// SupportedFormats contains supported repository formats.
var SupportedFormats = ObjectIDFormats{
// Non-encrypted formats.
&ObjectIDFormat{"md5", nonHMAC(md5.New), nil, nil},
&ObjectIDFormat{"hmac-md5", withHMAC(md5.New), nil, nil},
&ObjectIDFormat{"sha1", nonHMAC(sha1.New), nil, nil},
&ObjectIDFormat{"hmac-sha1", withHMAC(sha1.New), nil, nil},
&ObjectIDFormat{"sha256", nonHMAC(sha256.New), nil, nil},
&ObjectIDFormat{"hmac-sha256", withHMAC(sha256.New), nil, nil},
&ObjectIDFormat{"sha512-256", nonHMAC(sha512.New512_256), nil, nil},
&ObjectIDFormat{"hmac-sha512-256", withHMAC(sha512.New512_256), nil, nil},
&ObjectIDFormat{"sha512", nonHMAC(sha512.New), nil, nil},
&ObjectIDFormat{"hmac-sha512", withHMAC(sha512.New), nil, nil},
&ObjectIDFormat{"sha256-trunc128", nonHMAC(sha256.New), nil, splitKeyGenerator(16, 0)},
&ObjectIDFormat{"hmac-sha256-trunc128", withHMAC(sha256.New), nil, splitKeyGenerator(16, 0)},
&ObjectIDFormat{"sha512-trunc128", nonHMAC(sha512.New), nil, splitKeyGenerator(16, 0)},
&ObjectIDFormat{"hmac-sha512-trunc128", withHMAC(sha512.New), nil, splitKeyGenerator(16, 0)},
// Encrypted formats
&ObjectIDFormat{"hmac-sha512-aes256", withHMAC(sha512.New), aes.NewCipher, splitKeyGenerator(32, 32)},
&ObjectIDFormat{"hmac-sha384-aes256", withHMAC(sha512.New384), aes.NewCipher, splitKeyGenerator(16, 32)},
&ObjectIDFormat{"hmac-sha256-aes128", withHMAC(sha256.New), aes.NewCipher, splitKeyGenerator(16, 16)},
&ObjectIDFormat{"hmac-sha512-256-aes128", withHMAC(sha512.New512_256), aes.NewCipher, splitKeyGenerator(16, 16)},
func nonEncryptedFormat(name string, hf func() hash.Hash, hashSize int) *ObjectIDFormat {
return &ObjectIDFormat{
Name: name,
hashBuffer: func(data []byte, secret []byte) ([]byte, []byte) {
return hashContent(hf, data, secret)[0:hashSize], nil
},
}
}
func hashContent(hf func() hash.Hash, data []byte, secret []byte) []byte {
var h hash.Hash
if secret != nil {
h = hmac.New(hf, secret)
} else {
h = hf()
}
h.Write(data)
return h.Sum(nil)
}
func encryptedFormat(
name string,
hf func() hash.Hash,
blockIDSize int,
createCipher func(key []byte) (cipher.Block, error),
keySize int,
) *ObjectIDFormat {
return &ObjectIDFormat{
Name: name,
hashBuffer: func(data []byte, secret []byte) ([]byte, []byte) {
contentHash := hashContent(sha512.New, data, secret)
s1 := hkdf.New(sha256.New, contentHash, nil, hkdfInfoBlockID)
blockID := make([]byte, blockIDSize)
io.ReadFull(s1, blockID)
s2 := hkdf.New(sha256.New, contentHash, nil, hkdfInfoCryptoKey)
cryptoKey := make([]byte, keySize)
io.ReadFull(s2, cryptoKey)
return blockID, cryptoKey
},
createCipher: createCipher,
}
}
func buildObjectIDFormats() ObjectIDFormats {
var result ObjectIDFormats
for _, h := range []struct {
name string
hash func() hash.Hash
hashSize int
}{
{"md5", md5.New, md5.Size},
{"sha1", sha1.New, sha1.Size},
{"sha224", sha256.New224, sha256.Size224},
{"sha256", sha256.New, sha256.Size},
{"sha256t128", sha256.New, 16},
{"sha256t160", sha256.New, 20},
{"sha384", sha512.New384, sha512.Size384},
{"sha512t128", sha512.New, 16},
{"sha512t160", sha512.New, 20},
{"sha512-224", sha512.New512_224, sha512.Size224},
{"sha512-256", sha512.New512_256, sha512.Size256},
{"sha512", sha512.New, sha512.Size},
} {
result = append(result, nonEncryptedFormat(h.name, h.hash, h.hashSize))
result = append(result, encryptedFormat(h.name+"-aes128", h.hash, h.hashSize, aes.NewCipher, 16))
result = append(result, encryptedFormat(h.name+"-aes192", h.hash, h.hashSize, aes.NewCipher, 24))
result = append(result, encryptedFormat(h.name+"-aes256", h.hash, h.hashSize, aes.NewCipher, 32))
}
return result
}
// SupportedFormats contains supported repository formats.
var SupportedFormats = buildObjectIDFormats()

View File

@@ -97,7 +97,10 @@ func (w *objectWriter) flushBuffer(force bool) error {
length = w.buffer.Len()
}
objectID, readCloser := w.mgr.hashBufferForWriting(w.buffer, string(w.objectType)+w.prefix)
objectID, readCloser, err := w.mgr.hashBufferForWriting(w.buffer, string(w.objectType)+w.prefix)
if err != nil {
return err
}
w.buffer = nil
if err := w.mgr.storage.PutBlock(objectID.BlockID(), readCloser, blob.PutOptions{}); err != nil {
@@ -136,7 +139,7 @@ func (w *objectWriter) Result(forceStored bool) (ObjectID, error) {
return "B", nil
}
if w.buffer.Len() < w.mgr.maxInlineBlobSize {
if w.buffer.Len() < w.mgr.format.MaxInlineBlobSize {
return NewInlineObjectID(w.buffer.Bytes()), nil
}
}

View File

@@ -12,6 +12,10 @@
binaryEncoding = base64.RawURLEncoding
)
const (
objectIDEncryptionInfoSeparator = "."
)
// ObjectIDType describes the type of the chunk.
type ObjectIDType string
@@ -81,9 +85,9 @@ func (c ObjectID) InlineData() []byte {
func (c ObjectID) BlockID() string {
if c.Type().IsStored() {
content := string(c[1:])
firstColon := strings.Index(content, ":")
if firstColon > 0 {
return string(content[0:firstColon])
firstSeparator := strings.Index(content, objectIDEncryptionInfoSeparator)
if firstSeparator > 0 {
return string(content[0:firstSeparator])
}
return string(content)
@@ -96,9 +100,9 @@ func (c ObjectID) BlockID() string {
func (c ObjectID) EncryptionInfo() ObjectEncryptionInfo {
if c.Type().IsStored() {
content := string(c[1:])
firstColon := strings.Index(content, ":")
if firstColon > 0 {
return ObjectEncryptionInfo(content[firstColon+1:])
firstSeparator := strings.Index(content, objectIDEncryptionInfoSeparator)
if firstSeparator > 0 {
return ObjectEncryptionInfo(content[firstSeparator+1:])
}
}
@@ -137,16 +141,16 @@ func ParseObjectID(objectIDString string) (ObjectID, error) {
}
case ObjectIDTypeList, ObjectIDTypeStored:
firstColon := strings.Index(content, ":")
lastColon := strings.LastIndex(content, ":")
if firstColon == lastColon {
if firstColon == -1 {
// Found zero colons in the ID - no encryption info.
firstSeparator := strings.Index(content, objectIDEncryptionInfoSeparator)
lastSeparator := strings.LastIndex(content, objectIDEncryptionInfoSeparator)
if firstSeparator == lastSeparator {
if firstSeparator == -1 {
// Found zero Separators in the ID - no encryption info.
return ObjectID(objectIDString), nil
}
if firstColon > 0 {
b, err := hex.DecodeString(content[firstColon+1:])
if firstSeparator > 0 {
b, err := hex.DecodeString(content[firstSeparator+1:])
if err == nil && len(b) > 0 && len(b)%2 == 0 {
// Valid chunk ID with encryption info.
return ObjectID(objectIDString), nil

View File

@@ -10,20 +10,20 @@ func TestParseMalformedObjectID(t *testing.T) {
"",
"B!$@#$!@#$",
"X",
"D:",
"D:x",
"D:af",
"Dx:ag",
"Dab:",
"Dab:a",
"Dab:abc",
"Dab:00",
"Dab:011",
"L:",
"L:x",
"L:af",
"Lx:ag",
"Lab:",
"D.",
"D.x",
"D.af",
"Dx.ag",
"Dab.",
"Dab.a",
"Dab.abc",
"Dab.00",
"Dab.011",
"L.",
"L.x",
"L.af",
"Lx.ag",
"Lab.",
"Xsomething",
}
@@ -49,11 +49,11 @@ func TestParseObjectIDEncryptionInfo(t *testing.T) {
{"Dabcdef", NoEncryption},
{"Labcdef", NoEncryption},
{
"Dabcdef:00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
"Dabcdef.00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
ObjectEncryptionInfo("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"),
},
{
"Labcdef:00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
"Labcdef.00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
ObjectEncryptionInfo("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"),
},
}

View File

@@ -11,7 +11,6 @@
"hash"
"io"
"io/ioutil"
"log"
"strconv"
"strings"
"sync/atomic"
@@ -27,20 +26,6 @@
// Instead of using all-zero, we use this one.
var constantIV = []byte("kopiakopiakopiakopiakopiakopiakopiakopiakopiakopiakopiakopiakopiakopiakopiakopiakopiakopiakopiakopiakopiakopiakopiakopia")
func nonHMAC(hf func() hash.Hash) func(secret []byte) func() hash.Hash {
return func(secret []byte) func() hash.Hash {
return hf
}
}
func withHMAC(hf func() hash.Hash) func(secret []byte) func() hash.Hash {
return func(key []byte) func() hash.Hash {
return func() hash.Hash {
return hmac.New(hf, key)
}
}
}
// Repository objects addressed by their content allows reading and writing them.
type Repository interface {
// NewWriter opens an ObjectWriter for writing new content to the blob.
@@ -83,12 +68,8 @@ type repository struct {
bufferManager *bufferManager
stats RepositoryStats
maxInlineBlobSize int
maxBlobSize int
hashFunc func() hash.Hash
createCipher func([]byte) (cipher.Block, error)
keygen keygenFunc
format Format
idFormat ObjectIDFormat
}
func (mgr *repository) Close() {
@@ -200,24 +181,18 @@ func NewRepository(
if f.MaxBlobSize < 100 {
return nil, fmt.Errorf("MaxBlobSize is not set")
}
mgr := &repository{
storage: r,
maxInlineBlobSize: f.MaxInlineBlobSize,
maxBlobSize: f.MaxBlobSize,
}
if mgr.maxBlobSize == 0 {
mgr.maxBlobSize = 16 * 1024 * 1024
}
sf := SupportedFormats.Find(f.ObjectFormat)
if sf == nil {
return nil, fmt.Errorf("unknown object format: %v", f.ObjectFormat)
}
mgr.hashFunc = sf.hashFuncMaker(f.Secret)
mgr.createCipher = sf.createCipher
mgr.keygen = sf.keygen
mgr := &repository{
storage: r,
format: *f,
}
mgr.idFormat = *sf
for _, o := range options {
if err := o(mgr); err != nil {
@@ -226,44 +201,35 @@ func NewRepository(
}
}
mgr.bufferManager = newBufferManager(mgr.maxBlobSize)
mgr.bufferManager = newBufferManager(mgr.format.MaxBlobSize)
return mgr, nil
}
func splitKeyGenerator(blockIDSize int, keySize int) keygenFunc {
return func(b []byte) ([]byte, []byte) {
if len(b) < blockIDSize+keySize {
panic(fmt.Sprintf("hash result too short: %v, blockIDsize: %v keySize: %v", len(b), blockIDSize, keySize))
}
blockIDBytes := b[0:blockIDSize]
key := b[blockIDSize : blockIDSize+keySize]
return blockIDBytes, key
}
}
func (mgr *repository) hashBuffer(data []byte) ([]byte, []byte) {
h := mgr.hashFunc()
h.Write(data)
contentHash := h.Sum(nil)
if mgr.keygen != nil {
return mgr.keygen(contentHash)
}
return contentHash, nil
return mgr.idFormat.hashBuffer(data, mgr.format.Secret)
}
func (mgr *repository) hashBufferForWriting(buffer *bytes.Buffer, prefix string) (ObjectID, io.ReadCloser) {
func (mgr *repository) hashBufferForWriting(buffer *bytes.Buffer, prefix string) (ObjectID, io.ReadCloser, error) {
var data []byte
if buffer != nil {
data = buffer.Bytes()
}
var blockCipher cipher.Block
contentHash, cryptoKey := mgr.hashBuffer(data)
if cryptoKey != nil {
var err error
blockCipher, err = mgr.idFormat.createCipher(cryptoKey)
if err != nil {
return "", nil, err
}
}
var objectID ObjectID
if len(cryptoKey) > 0 {
objectID = ObjectID(prefix + hex.EncodeToString(contentHash) + ":" + hex.EncodeToString(cryptoKey))
objectID = ObjectID(prefix + hex.EncodeToString(contentHash) + objectIDEncryptionInfoSeparator + hex.EncodeToString(cryptoKey))
} else {
objectID = ObjectID(prefix + hex.EncodeToString(contentHash))
}
@@ -272,29 +238,23 @@ func (mgr *repository) hashBufferForWriting(buffer *bytes.Buffer, prefix string)
atomic.AddInt64(&mgr.stats.HashedBytes, int64(len(data)))
if buffer == nil {
return objectID, ioutil.NopCloser(bytes.NewBuffer(nil))
return objectID, ioutil.NopCloser(bytes.NewBuffer(nil)), nil
}
readCloser := mgr.bufferManager.returnBufferOnClose(buffer)
readCloser = newCountingReader(readCloser, &mgr.stats.BytesWrittenToStorage)
if mgr.createCipher != nil {
c, err := mgr.createCipher(cryptoKey)
if err != nil {
log.Printf("can't create cipher: %v", err)
panic("can't encrypt block")
}
if len(cryptoKey) > 0 {
// Since we're not sharing the key, all-zero IV is ok.
// We don't need to worry about separate MAC either, since hashing content produces object ID.
ctr := cipher.NewCTR(c, constantIV[0:c.BlockSize()])
ctr := cipher.NewCTR(blockCipher, constantIV[0:blockCipher.BlockSize()])
readCloser = newCountingReader(
newEncryptingReader(readCloser, nil, ctr, nil),
&mgr.stats.EncryptedBytes)
}
return objectID, readCloser
return objectID, readCloser, nil
}
func (mgr *repository) flattenListChunk(
@@ -377,16 +337,12 @@ func (mgr *repository) newRawReader(objectID ObjectID) (io.ReadSeeker, error) {
return bytes.NewReader(payload), nil
}
if mgr.createCipher == nil {
return nil, errors.New("encrypted object cannot be used with non-encrypted Repository")
}
cryptoKey, err := hex.DecodeString(string(objectID.EncryptionInfo()))
if err != nil {
return nil, errors.New("malformed encryption key")
}
blockCipher, err := mgr.createCipher(cryptoKey)
blockCipher, err := mgr.idFormat.createCipher(cryptoKey)
if err != nil {
return nil, errors.New("cannot create cipher")
}

View File

@@ -235,7 +235,7 @@ func TestHMAC(t *testing.T) {
content := bytes.Repeat([]byte{0xcd}, 50)
s := testFormat()
s.ObjectFormat = "hmac-md5"
s.ObjectFormat = "md5"
s.Secret = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25}
repo, err := NewRepository(blob.NewMapStorage(data), s)
@@ -390,17 +390,18 @@ func TestFormats(t *testing.T) {
oids map[string]ObjectID
}{
{
format: makeFormat("md5"),
format: makeFormat("md5"), // MD5-HMAC with secret 'key'
oids: map[string]ObjectID{
"": "Dd41d8cd98f00b204e9800998ecf8427e",
"The quick brown fox jumps over the lazy dog": "D9e107d9d372bb6826bd81d3542a419d6",
"": "D63530468a04e386459855da0063b6596",
"The quick brown fox jumps over the lazy dog": "D80070713463e7749b90c2dc24911e275",
},
},
{
format: &Format{
Version: "1",
ObjectFormat: "hmac-md5",
ObjectFormat: "md5",
MaxBlobSize: 10000,
Secret: []byte{}, // HMAC with zero-byte secret
},
oids: map[string]ObjectID{
"": "D74e6f7298a9c2d168935f58c001bad88",
@@ -408,50 +409,68 @@ func TestFormats(t *testing.T) {
},
},
{
format: makeFormat("hmac-md5"),
format: &Format{
Version: "1",
ObjectFormat: "md5",
MaxBlobSize: 10000,
Secret: nil, // non-HMAC version
},
oids: map[string]ObjectID{
"": "Dd41d8cd98f00b204e9800998ecf8427e",
"The quick brown fox jumps over the lazy dog": "D9e107d9d372bb6826bd81d3542a419d6",
},
},
{
format: makeFormat("md5"),
oids: map[string]ObjectID{
"The quick brown fox jumps over the lazy dog": "D80070713463e7749b90c2dc24911e275",
},
},
{
format: makeFormat("hmac-sha1"),
format: makeFormat("sha1"),
oids: map[string]ObjectID{
"The quick brown fox jumps over the lazy dog": "Dde7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9",
},
},
{
format: makeFormat("hmac-sha256"),
format: makeFormat("sha256"),
oids: map[string]ObjectID{
"The quick brown fox jumps over the lazy dog": "Df7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8",
},
},
{
format: makeFormat("hmac-sha512"),
format: makeFormat("sha512"),
oids: map[string]ObjectID{
"The quick brown fox jumps over the lazy dog": "Db42af09057bac1e2d41708e48a902e09b5ff7f12ab428a4fe86653c73dd248fb82f948a549f7b791a5b41915ee4d1ec3935357e4e2317250d0372afa2ebeeb3a",
},
},
{
format: makeFormat("hmac-sha256-aes128"),
format: makeFormat("sha256t128-aes128"),
oids: map[string]ObjectID{
"The quick brown fox jumps over the lazy dog": "Df7bc83f430538424b13298e6aa6fb143:ef4d59a14946175997479dbc2d1a3cd8",
"The quick brown fox jumps over the lazy dog": "D4e43650cb2ce7b5a6a2a8d614c13d9b3.f73b9fed1f355b3603e066fb24a39970",
},
},
{
format: makeFormat("hmac-sha384-aes256"),
format: makeFormat("sha256-aes128"),
oids: map[string]ObjectID{
"The quick brown fox jumps over the lazy dog": "Dd7f4727e2c0b39ae0f1e40cc96f60242:d5b7801841cea6fc592c5d3e1ae50700582a96cf35e1e554995fe4e03381c237",
"The quick brown fox jumps over the lazy dog": "D4e43650cb2ce7b5a6a2a8d614c13d9b37c6ee13c4543481074d2c8cf3f597a13.f73b9fed1f355b3603e066fb24a39970",
},
},
{
format: makeFormat("hmac-sha512-aes256"),
format: makeFormat("sha384-aes256"),
oids: map[string]ObjectID{
"The quick brown fox jumps over the lazy dog": "Db42af09057bac1e2d41708e48a902e09b5ff7f12ab428a4fe86653c73dd248fb:82f948a549f7b791a5b41915ee4d1ec3935357e4e2317250d0372afa2ebeeb3a",
"The quick brown fox jumps over the lazy dog": "D4e43650cb2ce7b5a6a2a8d614c13d9b37c6ee13c4543481074d2c8cf3f597a136dba4c0e67d5d644049c98ee5369bc0a.f73b9fed1f355b3603e066fb24a39970ac10cbd143babc7f487ef263fe38aeff",
},
},
{
format: makeFormat("sha512-aes256"),
oids: map[string]ObjectID{
"The quick brown fox jumps over the lazy dog": "D4e43650cb2ce7b5a6a2a8d614c13d9b37c6ee13c4543481074d2c8cf3f597a136dba4c0e67d5d644049c98ee5369bc0aaa0e75feaeadd42eb54a9d64f9d0a51d.f73b9fed1f355b3603e066fb24a39970ac10cbd143babc7f487ef263fe38aeff",
},
},
}
for _, c := range cases {
for caseIndex, c := range cases {
data := map[string][]byte{}
st := blob.NewMapStorage(data)
@@ -471,7 +490,7 @@ func TestFormats(t *testing.T) {
t.Errorf("error: %v", err)
}
if oid != v {
t.Errorf("invalid oid for %v/%v: %v expected %v", c.format.ObjectFormat, k, oid, v)
t.Errorf("invalid oid for #%v %v/%v: %v expected %v", caseIndex, c.format.ObjectFormat, k, oid, v)
}
rc, err := repo.Open(oid)
@@ -495,7 +514,7 @@ func TestInvalidEncryptionKey(t *testing.T) {
st := blob.NewMapStorage(data)
format := Format{
Version: "1",
ObjectFormat: "hmac-sha512-aes256",
ObjectFormat: "sha512-aes256",
Secret: []byte("key"),
MaxBlobSize: 1000,
}