github.com/vchain-us/vcn@v0.9.11-0.20210921212052-a2484d23c0b3/pkg/api/lc_artifact.go (about)

     1  /*
     2   * Copyright (c) 2018-2020 vChain, Inc. All Rights Reserved.
     3   * This software is released under GPL3.
     4   * The full license information can be found under:
     5   * https://www.gnu.org/licenses/gpl-3.0.en.html
     6   *
     7   */
     8  
     9  package api
    10  
    11  import (
    12  	"archive/zip"
    13  	"bytes"
    14  	"context"
    15  	"crypto/sha256"
    16  	"encoding/hex"
    17  	"encoding/json"
    18  	"errors"
    19  	"fmt"
    20  	"io"
    21  	"io/ioutil"
    22  	"math"
    23  	"net/http"
    24  	"os"
    25  	"path"
    26  	"regexp"
    27  	"sort"
    28  	"strconv"
    29  	"strings"
    30  	"time"
    31  
    32  	"github.com/spf13/viper"
    33  
    34  	immuschema "github.com/codenotary/immudb/pkg/api/schema"
    35  	"github.com/vchain-us/ledger-compliance-go/schema"
    36  	"github.com/vchain-us/vcn/pkg/meta"
    37  	"google.golang.org/grpc/metadata"
    38  	"google.golang.org/grpc/status"
    39  )
    40  
    41  func (a Artifact) toLcArtifact() *LcArtifact {
    42  	aR := &LcArtifact{
    43  		// root fields
    44  		Kind:        a.Kind,
    45  		Name:        a.Name,
    46  		Hash:        a.Hash,
    47  		Size:        a.Size,
    48  		ContentType: a.ContentType,
    49  
    50  		// custom metadata
    51  		Metadata: a.Metadata,
    52  	}
    53  
    54  	return aR
    55  }
    56  func ItemToLcArtifact(item *schema.ItemExt) (*LcArtifact, error) {
    57  	var lca LcArtifact
    58  	err := json.Unmarshal(item.Item.Value, &lca)
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  	ts := time.Unix(item.Timestamp.GetSeconds(), int64(item.Timestamp.GetNanos()))
    63  	lca.Uid = strconv.Itoa(int(ts.UnixNano()))
    64  	lca.Timestamp = ts.UTC()
    65  	// if ApikeyRevoked == nil no revoked infos available. Old key type
    66  	if item.ApikeyRevoked != nil {
    67  		if item.ApikeyRevoked.GetSeconds() > 0 {
    68  			t := time.Unix(item.ApikeyRevoked.GetSeconds(), int64(item.ApikeyRevoked.Nanos)).UTC()
    69  			lca.Revoked = &t
    70  		} else {
    71  			lca.Revoked = &time.Time{}
    72  		}
    73  	}
    74  	lca.Ledger = item.LedgerName
    75  	return &lca, nil
    76  }
    77  
    78  func ZItemToLcArtifact(ie *schema.ZItemExt) (*LcArtifact, error) {
    79  	var lca LcArtifact
    80  	err := json.Unmarshal(ie.Item.Entry.Value, &lca)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  	ts := time.Unix(ie.Timestamp.GetSeconds(), int64(ie.Timestamp.GetNanos()))
    85  	lca.Uid = strconv.Itoa(int(ts.UnixNano()))
    86  	lca.Timestamp = ts.UTC()
    87  	// if ApikeyRevoked == nil no revoked infos available. Old key type
    88  	if ie.ApikeyRevoked != nil {
    89  		if ie.ApikeyRevoked.GetSeconds() > 0 {
    90  			t := time.Unix(ie.ApikeyRevoked.GetSeconds(), int64(ie.ApikeyRevoked.Nanos)).UTC()
    91  			lca.Revoked = &t
    92  		} else {
    93  			lca.Revoked = &time.Time{}
    94  		}
    95  	}
    96  	lca.Ledger = ie.LedgerName
    97  	return &lca, nil
    98  }
    99  
   100  func VerifiableItemExtToLcArtifact(item *schema.VerifiableItemExt) (*LcArtifact, error) {
   101  	var lca LcArtifact
   102  	err := json.Unmarshal(item.Item.Entry.Value, &lca)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  	ts := time.Unix(item.Timestamp.GetSeconds(), int64(item.Timestamp.GetNanos()))
   107  	lca.Uid = strconv.Itoa(int(ts.UnixNano()))
   108  	lca.Timestamp = ts.UTC()
   109  	// if ApikeyRevoked == nil no revoked infos available. Old key type
   110  	if item.ApikeyRevoked != nil {
   111  		if item.ApikeyRevoked.GetSeconds() > 0 {
   112  			t := time.Unix(item.ApikeyRevoked.GetSeconds(), int64(item.ApikeyRevoked.Nanos)).UTC()
   113  			lca.Revoked = &t
   114  		} else {
   115  			lca.Revoked = &time.Time{}
   116  		}
   117  	}
   118  	lca.Ledger = item.LedgerName
   119  	return &lca, nil
   120  }
   121  
   122  type LcArtifact struct {
   123  	// root fields
   124  	Uid         string    `json:"uid" yaml:"uid" vcn:"UID"`
   125  	Kind        string    `json:"kind" yaml:"kind" vcn:"Kind"`
   126  	Name        string    `json:"name" yaml:"name" vcn:"Name"`
   127  	Hash        string    `json:"hash" yaml:"hash" vcn:"Hash"`
   128  	Size        uint64    `json:"size" yaml:"size" vcn:"Size"`
   129  	Timestamp   time.Time `json:"timestamp,omitempty" yaml:"timestamp" vcn:"Timestamp"`
   130  	ContentType string    `json:"contentType" yaml:"contentType" vcn:"ContentType"`
   131  
   132  	// custom metadata
   133  	Metadata    Metadata     `json:"metadata" yaml:"metadata" vcn:"Metadata"`
   134  	Attachments []Attachment `json:"attachments" yaml:"attachments" vcn:"Attachments"`
   135  
   136  	Signer  string      `json:"signer" yaml:"signer" vcn:"SignerID"`
   137  	Revoked *time.Time  `json:"revoked,omitempty" yaml:"revoked" vcn:"Apikey revoked"`
   138  	Status  meta.Status `json:"status" yaml:"status" vcn:"Status"`
   139  	Ledger  string      `json:"ledger,omitempty" yaml:"ledger"`
   140  
   141  	IncludedIn []PackageDetails `json:"included_in,omitempty" yaml:"included_in,omitempty" vcn:"Included in"`
   142  	Deps       []PackageDetails `json:"bom,omitempty" yaml:"bom,omitempty" vcn:"Dependencies"`
   143  }
   144  
   145  func (u LcUser) artifactToSetRequest(
   146  	artifact *Artifact,
   147  	status meta.Status,
   148  	attach []string,
   149  	bomText string,
   150  	outReq *immuschema.SetRequest,
   151  ) error {
   152  
   153  	aR := artifact.toLcArtifact()
   154  	aR.Status = status
   155  
   156  	aR.Signer = GetSignerIDByApiKey(u.Client.ApiKey)
   157  
   158  	// vcn.myApiKey.{artifact hash}
   159  	// attachment key need to have "vcn." prefix because it's handled inside cnil frontend. (attachment is listed in the UI).
   160  	key := AppendPrefix(meta.VcnPrefix, []byte(aR.Signer))
   161  	key = AppendSignerId(artifact.Hash, key)
   162  
   163  	// Attachments handler
   164  	// attachments info generation and multi kv preparation
   165  	var aKVs []*immuschema.KeyValue
   166  	var aRattachment []Attachment
   167  
   168  	if bomText != "" {
   169  		kv := &immuschema.KeyValue{
   170  			Key:   []byte(meta.BomEntryKeyName),
   171  			Value: []byte(bomText),
   172  		}
   173  
   174  		aKVs = append(aKVs, kv)
   175  	}
   176  
   177  	// map to save all the attachments with a specific label
   178  	labelMap := make(map[string][]Attachment)
   179  
   180  	if viper.GetBool("compress") && len(attach) > 0 {
   181  		zipBuf := new(bytes.Buffer)
   182  
   183  		z := zip.NewWriter(zipBuf)
   184  		for _, a := range attach {
   185  			// In case of compressed attachments ignore the optional tag
   186  			fields := strings.SplitN(a, ":", 2)
   187  			if len(fields) > 1 {
   188  				return errors.New("cannot specify tag for attachments to be compressed")
   189  			}
   190  			err := compressFile(z, fields[0])
   191  			if err != nil {
   192  				return err
   193  			}
   194  		}
   195  		err := z.Close()
   196  		if err != nil {
   197  			return err
   198  		}
   199  
   200  		checksum := sha256.Sum256(zipBuf.Bytes())
   201  		hash := hex.EncodeToString(checksum[:])
   202  		akey := AppendAttachment(hash, key)
   203  
   204  		kv := &immuschema.KeyValue{
   205  			Key:   []byte(akey),
   206  			Value: zipBuf.Bytes(),
   207  		}
   208  
   209  		aKVs = append(aKVs, kv)
   210  
   211  		at := Attachment{
   212  			Filename: "attachments.zip",
   213  			Hash:     hash,
   214  			Mime:     "application/zip",
   215  		}
   216  		aRattachment = append(aRattachment, at)
   217  	} else {
   218  		for _, al := range attach {
   219  			// attachment can be --attach=vscanner.result:jobid123. jobid123 is the label
   220  			alSlice := strings.SplitN(al, ":", 2)
   221  			a := alSlice[0]
   222  			/** friendly label **/
   223  			label := ""
   224  			if len(alSlice) > 1 {
   225  				label = alSlice[1]
   226  			}
   227  
   228  			// attachment
   229  			f, err := os.Open(a)
   230  			if err != nil {
   231  				return err
   232  			}
   233  			defer f.Close()
   234  
   235  			fc, err := ioutil.ReadFile(a)
   236  			if err != nil {
   237  				return err
   238  			}
   239  			h := sha256.New()
   240  			if _, err := io.Copy(h, f); err != nil {
   241  				return err
   242  			}
   243  			checksum := h.Sum(nil)
   244  			hash := hex.EncodeToString(checksum)
   245  			akey := AppendAttachment(hash, key)
   246  
   247  			kv := &immuschema.KeyValue{
   248  				Key:   []byte(akey),
   249  				Value: fc,
   250  			}
   251  
   252  			aKVs = append(aKVs, kv)
   253  
   254  			mime := http.DetectContentType(fc)
   255  			at := Attachment{
   256  				Filename: path.Base(a),
   257  				Hash:     hash,
   258  				Mime:     mime,
   259  				Label:    label,
   260  			}
   261  
   262  			/** friendly label **/
   263  			/* _ITEM.ATTACH.LABEL.myApiKey.{arifact hash}.vscanner.result:jobid123 */
   264  			if label != "" {
   265  				labelKey := meta.VcnAttachmentLabelPrefix + "." + aR.Signer + "." + artifact.Hash + "." + al
   266  				// here is used an array to be downloadable by the same code in the attachments map use case
   267  				attachs := []Attachment{at}
   268  				attachmentsListJSON, err := json.Marshal(attachs)
   269  				if err != nil {
   270  					return err
   271  				}
   272  				labelKV := &immuschema.KeyValue{
   273  					Key:   []byte(labelKey),
   274  					Value: attachmentsListJSON,
   275  				}
   276  				aKVs = append(aKVs, labelKV)
   277  
   278  				// label map
   279  				// append the attachment key in the labelMap at specific label key
   280  				labelMap[label] = append(labelMap[label], at)
   281  			}
   282  
   283  			aRattachment = append(aRattachment, at)
   284  		}
   285  	}
   286  
   287  	aR.Attachments = aRattachment
   288  	arJSON, err := json.Marshal(aR)
   289  	if err != nil {
   290  		return err
   291  	}
   292  
   293  	outReq.KVs = []*immuschema.KeyValue{{Key: key, Value: arJSON}}
   294  	if len(aKVs) > 0 {
   295  		outReq.KVs = append(outReq.KVs, aKVs...)
   296  	}
   297  
   298  	// here is built a key to retrieve in a single call all the attachment with a specific label. The value is a list of attachment keys joined by ":" separator
   299  	for label, attachments := range labelMap {
   300  		/* _ITEM.ATTACH.LABEL.myApiKey.{arifact hash}.jobid123 */
   301  		labelMapKey := meta.VcnAttachmentLabelPrefix + "." + aR.Signer + "." + artifact.Hash + "." + label
   302  
   303  		attachmentsListJSON, err := json.Marshal(attachments)
   304  		if err != nil {
   305  			return err
   306  		}
   307  		labelMapKV := &immuschema.KeyValue{
   308  			Key:   []byte(labelMapKey),
   309  			Value: attachmentsListJSON, // attachmentKeys
   310  		}
   311  
   312  		outReq.KVs = append(outReq.KVs, labelMapKV)
   313  	}
   314  
   315  	return nil
   316  }
   317  
   318  func (u LcUser) createArtifact(
   319  	artifact Artifact,
   320  	status meta.Status,
   321  	attach []string,
   322  	bomText string,
   323  ) (bool, uint64, error) {
   324  
   325  	var setRequest immuschema.SetRequest
   326  	if err := u.artifactToSetRequest(
   327  		&artifact, status, attach, bomText, &setRequest); err != nil {
   328  		return false, 0, err
   329  	}
   330  
   331  	md := metadata.Pairs(
   332  		meta.VcnLCPluginTypeHeaderName, meta.VcnLCPluginTypeHeaderValue,
   333  		meta.VcnLCCmdHeaderName, meta.VcnLCNotarizeCmdHeaderValue,
   334  	)
   335  	ctx := metadata.NewOutgoingContext(context.Background(), md)
   336  
   337  	txMeta, err := u.Client.SetAll(ctx, &setRequest)
   338  	if err != nil {
   339  		return false, 0, err
   340  	}
   341  	return true, txMeta.Id, nil
   342  }
   343  
   344  func (u LcUser) createArtifacts(
   345  	artifacts []*Artifact,
   346  	statuses []meta.Status,
   347  	attachments [][]string,
   348  	bomTexts []string,
   349  ) ([]bool, []uint64, []error, error) {
   350  
   351  	if len(artifacts) != len(statuses) || len(artifacts) != len(attachments) || len(artifacts) != len(bomTexts) {
   352  		return nil, nil, nil, errors.New(
   353  			"artifacts, statuses, attachments and bomTexts must have the same length")
   354  	}
   355  
   356  	verifieds := make([]bool, len(artifacts))
   357  	txIDs := make([]uint64, len(artifacts))
   358  	errs := make([]error, len(artifacts))
   359  
   360  	for i := 0; i < len(artifacts); i++ {
   361  		var setRequest immuschema.SetRequest
   362  		if err := u.artifactToSetRequest(
   363  			artifacts[i], statuses[i], attachments[i], bomTexts[i], &setRequest); err != nil {
   364  			return nil, nil, nil, err
   365  		}
   366  
   367  		md := metadata.Pairs(
   368  			meta.VcnLCPluginTypeHeaderName, meta.VcnLCPluginTypeHeaderValue,
   369  			meta.VcnLCCmdHeaderName, meta.VcnLCNotarizeCmdHeaderValue,
   370  		)
   371  		ctx := metadata.NewOutgoingContext(context.Background(), md)
   372  
   373  		txMeta, err := u.Client.SetAll(ctx, &setRequest)
   374  		if err != nil {
   375  			errs[i] = err
   376  			continue
   377  		}
   378  
   379  		verifieds[i] = true
   380  		txIDs[i] = txMeta.Id
   381  	}
   382  
   383  	return verifieds, txIDs, errs, nil
   384  }
   385  
   386  // LoadArtifact fetches and returns an *lcArtifact for the given hash and current u, if any.
   387  func (u *LcUser) LoadArtifact(
   388  	hash, signerID string,
   389  	uid string,
   390  	tx uint64,
   391  	gRPCMetadata map[string][]string,
   392  ) (lc *LcArtifact, verified bool, err error) {
   393  
   394  	md := metadata.Pairs(meta.VcnLCPluginTypeHeaderName, meta.VcnLCPluginTypeHeaderValue)
   395  	if len(gRPCMetadata) > 0 {
   396  		md = metadata.Join(md, gRPCMetadata)
   397  	}
   398  	ctx := metadata.NewOutgoingContext(context.Background(), md)
   399  
   400  	if signerID == "" {
   401  		signerID = GetSignerIDByApiKey(u.Client.ApiKey)
   402  	}
   403  
   404  	key := AppendPrefix(meta.VcnPrefix, []byte(signerID))
   405  	key = AppendSignerId(hash, key)
   406  
   407  	var jsonAr *schema.VerifiableItemExt
   408  	if uid != "" {
   409  		score, err := strconv.ParseFloat(uid, 64)
   410  		if err != nil {
   411  			return nil, false, err
   412  		}
   413  		zitems, err := u.Client.ZScanExt(ctx, &immuschema.ZScanRequest{
   414  			Set:       key,
   415  			SeekScore: math.MaxFloat64,
   416  			SeekAtTx:  tx,
   417  			Limit:     1,
   418  			MinScore:  &immuschema.Score{Score: score},
   419  			MaxScore:  &immuschema.Score{Score: score},
   420  			SinceTx:   math.MaxUint64,
   421  			NoWait:    true,
   422  		})
   423  		if err != nil {
   424  			return nil, false, err
   425  		}
   426  		if len(zitems.Items) > 0 {
   427  			jsonAr, err = u.Client.VerifiedGetExtAt(ctx, zitems.Items[0].Item.Key, zitems.Items[0].Item.AtTx)
   428  		} else {
   429  			return nil, false, ErrNotFound
   430  		}
   431  	} else {
   432  		jsonAr, err = u.Client.VerifiedGetExtAt(ctx, key, tx)
   433  	}
   434  	if err != nil {
   435  		s, ok := status.FromError(err)
   436  		if ok && s.Message() == "data is corrupted" {
   437  			return nil, false, ErrNotVerified
   438  		}
   439  		if err.Error() == "data is corrupted" {
   440  			return nil, false, ErrNotVerified
   441  		}
   442  		if ok && s.Message() == "key not found" {
   443  			return nil, false, ErrNotFound
   444  		}
   445  		return nil, true, err
   446  	}
   447  
   448  	lcArtifact, err := VerifiableItemExtToLcArtifact(jsonAr)
   449  	if err != nil {
   450  		return nil, false, err
   451  	}
   452  
   453  	return lcArtifact, true, nil
   454  }
   455  
   456  // LoadArtifacts fetches and returns multiple *lcArtifact for the given hashes and current u, if any.
   457  func (u *LcUser) LoadArtifacts(
   458  	signerID string,
   459  	hashes []string,
   460  	gRPCMetadata map[string][]string,
   461  ) (artifacts []*LcArtifact, verified []bool, errs []error, err error) {
   462  
   463  	md := metadata.Pairs(meta.VcnLCPluginTypeHeaderName, meta.VcnLCPluginTypeHeaderValue)
   464  	if len(gRPCMetadata) > 0 {
   465  		md = metadata.Join(md, gRPCMetadata)
   466  	}
   467  	ctx := metadata.NewOutgoingContext(context.Background(), md)
   468  
   469  	if signerID == "" {
   470  		signerID = GetSignerIDByApiKey(u.Client.ApiKey)
   471  	}
   472  
   473  	prefixedSignerID := AppendPrefix(meta.VcnPrefix, []byte(signerID))
   474  
   475  	keys := make([][]byte, 0, len(hashes))
   476  	for _, hash := range hashes {
   477  		key := AppendSignerId(hash, prefixedSignerID)
   478  		keys = append(keys, key)
   479  	}
   480  
   481  	itemsExt, errsMsgs, err := u.Client.VerifiedGetExtAtMulti(ctx, keys, make([]uint64, len(keys)))
   482  	if err != nil {
   483  		return nil, nil, nil, err
   484  	}
   485  
   486  	if len(itemsExt) != len(keys) || len(errsMsgs) != len(keys) {
   487  		return nil, nil, nil, fmt.Errorf(
   488  			"internal logic error: expected size of the reponse %d, got %d items and %d errors",
   489  			len(keys), len(itemsExt), len(errsMsgs))
   490  	}
   491  
   492  	lcArtifacts := make([]*LcArtifact, len(itemsExt))
   493  	verified = make([]bool, len(itemsExt))
   494  	errs = make([]error, len(itemsExt))
   495  
   496  	for i := 0; i < len(keys); i++ {
   497  		if len(errsMsgs[i]) > 0 {
   498  			switch {
   499  			case strings.HasSuffix(errsMsgs[i], "data is corrupted"):
   500  				errs[i] = ErrNotVerified
   501  			case strings.HasSuffix(errsMsgs[i], "key not found"):
   502  				errs[i] = ErrNotFound
   503  			default:
   504  				errs[i] = errors.New(errsMsgs[i])
   505  				verified[i] = true
   506  			}
   507  			continue
   508  		}
   509  
   510  		lcArtifact, err := VerifiableItemExtToLcArtifact(itemsExt[i])
   511  		if err != nil {
   512  			return nil, nil, nil, err
   513  		}
   514  		lcArtifacts[i] = lcArtifact
   515  		verified[i] = true
   516  	}
   517  
   518  	return lcArtifacts, verified, errs, nil
   519  }
   520  
   521  // GetArtifactAttachmentListByLabel returns the attachment list of an artifact and the most recent uid by a provided label and signerID
   522  // When there are multiple attachments with same file name it adds an enumerator postfix.
   523  func (u *LcUser) GetArtifactAttachmentListByLabel(hash string, signerID, label string) ([]Attachment, string, error) {
   524  	if label == "" {
   525  		return nil, "", errors.New("no attachment provided")
   526  	}
   527  	if hash == "" {
   528  		return nil, "", errors.New("no artifact provided")
   529  	}
   530  	var attachmentList []Attachment
   531  	var uid string
   532  	attachmentMap, err := u.fetchAttachmentMapByLabel(hash, signerID, label)
   533  	if err != nil {
   534  		return nil, "", err
   535  	}
   536  	// map order is not guaranted so here obtain a sorted string array
   537  	var attachDriver []string
   538  	for k, _ := range attachmentMap {
   539  		attachDriver = append(attachDriver, k)
   540  	}
   541  	sort.Strings(attachDriver)
   542  	// reverse the driver
   543  	last := len(attachDriver) - 1
   544  	for i := 0; i < len(attachDriver)/2; i++ {
   545  		attachDriver[i], attachDriver[last-i] = attachDriver[last-i], attachDriver[i]
   546  	}
   547  	// attachmentFileNameMap is used internally to produce a map to handle attachments with same name
   548  	attachmentFileNameMap := make(map[string][]*Attachment)
   549  
   550  	for _, k := range attachDriver {
   551  		attachMapEntry := attachmentMap[k]
   552  		// latest uid, needed to authenticate the latest notarized artifact
   553  		if uid == "" {
   554  			uid = k
   555  		}
   556  		for _, att := range attachMapEntry {
   557  			fn := att.Filename
   558  			if _, ok := attachmentFileNameMap[fn]; ok && len(attachmentFileNameMap[fn]) > 0 {
   559  				// if there is a newer filename here a postfix is added. ~1,~2 ... ~N
   560  				att.Filename = fn + "~" + strconv.Itoa(len(attachmentFileNameMap[fn]))
   561  			}
   562  			attachmentFileNameMap[fn] = append(attachmentFileNameMap[fn], att)
   563  			// attachmentList contains all attachments with latest first order
   564  			attachmentList = append(attachmentList, *att)
   565  		}
   566  	}
   567  	return attachmentList, uid, nil
   568  }
   569  
   570  func (u *LcUser) fetchAttachmentMapByLabel(hash, signerID string, attach string) (map[string][]*Attachment, error) {
   571  
   572  	md := metadata.Pairs(meta.VcnLCPluginTypeHeaderName, meta.VcnLCPluginTypeHeaderValue)
   573  	ctx := metadata.NewOutgoingContext(context.Background(), md)
   574  
   575  	if signerID == "" {
   576  		signerID = GetSignerIDByApiKey(u.Client.ApiKey)
   577  	}
   578  
   579  	key := meta.VcnAttachmentLabelPrefix + "." + signerID + "." + hash + "." + attach
   580  
   581  	/* _ITEM.ATTACH.LABEL.myApiKey.{arifact hash}.vscanner.result:jobid123 */
   582  	/* _ITEM.ATTACH.LABEL.myApiKey.{arifact hash}.jobid123 */
   583  	sr := &immuschema.ScanRequest{
   584  		Prefix:  []byte(key),
   585  		SinceTx: math.MaxUint64,
   586  		NoWait:  true,
   587  		Desc:    true,
   588  	}
   589  
   590  	res, err := u.Client.Scan(ctx, sr)
   591  	if err != nil {
   592  		return nil, err
   593  	}
   594  	if len(res.Entries) < 1 {
   595  		return nil, errors.New("provided label does not contains entries")
   596  	}
   597  
   598  	attachMap := make(map[string][]*Attachment)
   599  
   600  	for _, entry := range res.Entries {
   601  		// ori reg ex _ITEM\.ATTACH\.LABEL\.[^.]+\.[^.]+\.(\S+:\S[^.]+|\S+)\.([0-9]+)
   602  		var regex = regexp.MustCompile("_ITEM\\.ATTACH\\.LABEL\\.[^.]+\\.[^.]+\\.(\\S+:\\S[^.]+|\\S+)\\.([0-9]+)")
   603  		keyAndUid := regex.FindStringSubmatch(string(entry.Key))
   604  
   605  		if len(keyAndUid) != 3 {
   606  			return nil, errors.New("not consistent data when retrieving uid from attachment label entry")
   607  		}
   608  
   609  		attachmentList := make([]*Attachment, 0)
   610  		err = json.Unmarshal(entry.Value, &attachmentList)
   611  		if err != nil {
   612  			return nil, err
   613  		}
   614  		attachMap[keyAndUid[2]] = attachmentList
   615  	}
   616  
   617  	return attachMap, nil
   618  }
   619  
   620  func AppendPrefix(prefix string, key []byte) []byte {
   621  	var prefixed = make([]byte, len(prefix)+1+len(key))
   622  	copy(prefixed[0:], prefix+".")
   623  	copy(prefixed[len(prefix)+1:], key)
   624  	return prefixed
   625  }
   626  
   627  func AppendSignerId(signerId string, k []byte) []byte {
   628  	var prefixed = make([]byte, len(k)+len(signerId)+1)
   629  	copy(prefixed[0:], k)
   630  	copy(prefixed[len(k):], "."+signerId)
   631  	return prefixed
   632  }
   633  
   634  func AppendAttachment(attachHash string, key []byte) []byte {
   635  	//vcn.$AssetHash.Attachment.$AttachmentHash
   636  	var prefixed = make([]byte, len(attachHash)+len(meta.AttachmentSeparator)+len(key))
   637  	copy(prefixed[0:], key)
   638  	copy(prefixed[len(key):], meta.AttachmentSeparator+attachHash)
   639  	return prefixed
   640  }
   641  
   642  func AppendLabel(label string, key []byte) []byte {
   643  	//vcn.$AssetHash.Attachment.$AttachmentHash
   644  	var prefixed = make([]byte, len(label)+len(meta.AttachmentSeparator)+len(key))
   645  	copy(prefixed[0:], key)
   646  	copy(prefixed[len(key):], meta.AttachmentSeparator+label)
   647  	return prefixed
   648  }
   649  
   650  // DownloadAttachment download locally all the attachments linked to the assets
   651  func (u *LcUser) DownloadAttachment(attach *Attachment, ar *LcArtifact, tx uint64, lcAttachForce bool) (err error) {
   652  
   653  	md := metadata.Pairs(meta.VcnLCPluginTypeHeaderName, meta.VcnLCPluginTypeHeaderValue)
   654  	ctx := metadata.NewOutgoingContext(context.Background(), md)
   655  
   656  	key := AppendPrefix(meta.VcnPrefix, []byte(ar.Signer))
   657  	key = AppendSignerId(ar.Hash, key)
   658  	attachmentKey := AppendAttachment(attach.Hash, key)
   659  
   660  	attachEntry, err := u.Client.VerifiedGetAt(ctx, attachmentKey, tx)
   661  	if err != nil {
   662  		return err
   663  	}
   664  	if _, err := os.Stat(attach.Filename); os.IsNotExist(err) || lcAttachForce {
   665  		return ioutil.WriteFile(attach.Filename, attachEntry.Value, 0644)
   666  	}
   667  	return fmt.Errorf("attachment %s already present on disk. Use --force to overwrite silently", attach.Filename)
   668  }
   669  
   670  // Date returns a RFC3339 formatted string of verification time (v.Timestamp), if any, otherwise an empty string.
   671  func (lca *LcArtifact) Date() string {
   672  	if lca != nil {
   673  		ut := lca.Timestamp.UTC()
   674  		if ut.Unix() > 0 {
   675  			return ut.Format(time.RFC3339)
   676  		}
   677  	}
   678  	return ""
   679  }
   680  
   681  func compressFile(z *zip.Writer, name string) error {
   682  	src, err := os.Open(name)
   683  	if err != nil {
   684  		return err
   685  	}
   686  	defer src.Close()
   687  	fs, err := src.Stat()
   688  	if err != nil {
   689  		return err
   690  	}
   691  	header, err := zip.FileInfoHeader(fs)
   692  	if err != nil {
   693  		return err
   694  	}
   695  	header.Method = zip.Deflate        // it is Store by default
   696  	dst, err := z.CreateHeader(header) // use of CreateHeader rather then just Create keeps file time and mode
   697  	if err != nil {
   698  		return err
   699  	}
   700  	if _, err = io.Copy(dst, src); err != nil {
   701  		return err
   702  	}
   703  
   704  	return nil
   705  }