| package metadata // import "github.com/docker/docker/distribution/metadata" |
| |
| import ( |
| "crypto/hmac" |
| "crypto/sha256" |
| "encoding/hex" |
| "encoding/json" |
| "errors" |
| |
| "github.com/docker/docker/api/types" |
| "github.com/docker/docker/layer" |
| "github.com/opencontainers/go-digest" |
| ) |
| |
| // V2MetadataService maps layer IDs to a set of known metadata for |
| // the layer. |
| type V2MetadataService interface { |
| GetMetadata(diffID layer.DiffID) ([]V2Metadata, error) |
| GetDiffID(dgst digest.Digest) (layer.DiffID, error) |
| Add(diffID layer.DiffID, metadata V2Metadata) error |
| TagAndAdd(diffID layer.DiffID, hmacKey []byte, metadata V2Metadata) error |
| Remove(metadata V2Metadata) error |
| } |
| |
| // v2MetadataService implements V2MetadataService |
| type v2MetadataService struct { |
| store Store |
| } |
| |
| var _ V2MetadataService = &v2MetadataService{} |
| |
| // V2Metadata contains the digest and source repository information for a layer. |
| type V2Metadata struct { |
| Digest digest.Digest |
| SourceRepository string |
| // HMAC hashes above attributes with recent authconfig digest used as a key in order to determine matching |
| // metadata entries accompanied by the same credentials without actually exposing them. |
| HMAC string |
| } |
| |
| // CheckV2MetadataHMAC returns true if the given "meta" is tagged with a hmac hashed by the given "key". |
| func CheckV2MetadataHMAC(meta *V2Metadata, key []byte) bool { |
| if len(meta.HMAC) == 0 || len(key) == 0 { |
| return len(meta.HMAC) == 0 && len(key) == 0 |
| } |
| mac := hmac.New(sha256.New, key) |
| mac.Write([]byte(meta.Digest)) |
| mac.Write([]byte(meta.SourceRepository)) |
| expectedMac := mac.Sum(nil) |
| |
| storedMac, err := hex.DecodeString(meta.HMAC) |
| if err != nil { |
| return false |
| } |
| |
| return hmac.Equal(storedMac, expectedMac) |
| } |
| |
| // ComputeV2MetadataHMAC returns a hmac for the given "meta" hash by the given key. |
| func ComputeV2MetadataHMAC(key []byte, meta *V2Metadata) string { |
| if len(key) == 0 || meta == nil { |
| return "" |
| } |
| mac := hmac.New(sha256.New, key) |
| mac.Write([]byte(meta.Digest)) |
| mac.Write([]byte(meta.SourceRepository)) |
| return hex.EncodeToString(mac.Sum(nil)) |
| } |
| |
| // ComputeV2MetadataHMACKey returns a key for the given "authConfig" that can be used to hash v2 metadata |
| // entries. |
| func ComputeV2MetadataHMACKey(authConfig *types.AuthConfig) ([]byte, error) { |
| if authConfig == nil { |
| return nil, nil |
| } |
| key := authConfigKeyInput{ |
| Username: authConfig.Username, |
| Password: authConfig.Password, |
| Auth: authConfig.Auth, |
| IdentityToken: authConfig.IdentityToken, |
| RegistryToken: authConfig.RegistryToken, |
| } |
| buf, err := json.Marshal(&key) |
| if err != nil { |
| return nil, err |
| } |
| return []byte(digest.FromBytes(buf)), nil |
| } |
| |
| // authConfigKeyInput is a reduced AuthConfig structure holding just relevant credential data eligible for |
| // hmac key creation. |
| type authConfigKeyInput struct { |
| Username string `json:"username,omitempty"` |
| Password string `json:"password,omitempty"` |
| Auth string `json:"auth,omitempty"` |
| |
| IdentityToken string `json:"identitytoken,omitempty"` |
| RegistryToken string `json:"registrytoken,omitempty"` |
| } |
| |
| // maxMetadata is the number of metadata entries to keep per layer DiffID. |
| const maxMetadata = 50 |
| |
| // NewV2MetadataService creates a new diff ID to v2 metadata mapping service. |
| func NewV2MetadataService(store Store) V2MetadataService { |
| return &v2MetadataService{ |
| store: store, |
| } |
| } |
| |
| func (serv *v2MetadataService) diffIDNamespace() string { |
| return "v2metadata-by-diffid" |
| } |
| |
| func (serv *v2MetadataService) digestNamespace() string { |
| return "diffid-by-digest" |
| } |
| |
| func (serv *v2MetadataService) diffIDKey(diffID layer.DiffID) string { |
| return string(digest.Digest(diffID).Algorithm()) + "/" + digest.Digest(diffID).Hex() |
| } |
| |
| func (serv *v2MetadataService) digestKey(dgst digest.Digest) string { |
| return string(dgst.Algorithm()) + "/" + dgst.Hex() |
| } |
| |
| // GetMetadata finds the metadata associated with a layer DiffID. |
| func (serv *v2MetadataService) GetMetadata(diffID layer.DiffID) ([]V2Metadata, error) { |
| if serv.store == nil { |
| return nil, errors.New("no metadata storage") |
| } |
| jsonBytes, err := serv.store.Get(serv.diffIDNamespace(), serv.diffIDKey(diffID)) |
| if err != nil { |
| return nil, err |
| } |
| |
| var metadata []V2Metadata |
| if err := json.Unmarshal(jsonBytes, &metadata); err != nil { |
| return nil, err |
| } |
| |
| return metadata, nil |
| } |
| |
| // GetDiffID finds a layer DiffID from a digest. |
| func (serv *v2MetadataService) GetDiffID(dgst digest.Digest) (layer.DiffID, error) { |
| if serv.store == nil { |
| return layer.DiffID(""), errors.New("no metadata storage") |
| } |
| diffIDBytes, err := serv.store.Get(serv.digestNamespace(), serv.digestKey(dgst)) |
| if err != nil { |
| return layer.DiffID(""), err |
| } |
| |
| return layer.DiffID(diffIDBytes), nil |
| } |
| |
| // Add associates metadata with a layer DiffID. If too many metadata entries are |
| // present, the oldest one is dropped. |
| func (serv *v2MetadataService) Add(diffID layer.DiffID, metadata V2Metadata) error { |
| if serv.store == nil { |
| // Support a service which has no backend storage, in this case |
| // an add becomes a no-op. |
| // TODO: implement in memory storage |
| return nil |
| } |
| oldMetadata, err := serv.GetMetadata(diffID) |
| if err != nil { |
| oldMetadata = nil |
| } |
| newMetadata := make([]V2Metadata, 0, len(oldMetadata)+1) |
| |
| // Copy all other metadata to new slice |
| for _, oldMeta := range oldMetadata { |
| if oldMeta != metadata { |
| newMetadata = append(newMetadata, oldMeta) |
| } |
| } |
| |
| newMetadata = append(newMetadata, metadata) |
| |
| if len(newMetadata) > maxMetadata { |
| newMetadata = newMetadata[len(newMetadata)-maxMetadata:] |
| } |
| |
| jsonBytes, err := json.Marshal(newMetadata) |
| if err != nil { |
| return err |
| } |
| |
| err = serv.store.Set(serv.diffIDNamespace(), serv.diffIDKey(diffID), jsonBytes) |
| if err != nil { |
| return err |
| } |
| |
| return serv.store.Set(serv.digestNamespace(), serv.digestKey(metadata.Digest), []byte(diffID)) |
| } |
| |
| // TagAndAdd amends the given "meta" for hmac hashed by the given "hmacKey" and associates it with a layer |
| // DiffID. If too many metadata entries are present, the oldest one is dropped. |
| func (serv *v2MetadataService) TagAndAdd(diffID layer.DiffID, hmacKey []byte, meta V2Metadata) error { |
| meta.HMAC = ComputeV2MetadataHMAC(hmacKey, &meta) |
| return serv.Add(diffID, meta) |
| } |
| |
| // Remove disassociates a metadata entry from a layer DiffID. |
| func (serv *v2MetadataService) Remove(metadata V2Metadata) error { |
| if serv.store == nil { |
| // Support a service which has no backend storage, in this case |
| // an remove becomes a no-op. |
| // TODO: implement in memory storage |
| return nil |
| } |
| diffID, err := serv.GetDiffID(metadata.Digest) |
| if err != nil { |
| return err |
| } |
| oldMetadata, err := serv.GetMetadata(diffID) |
| if err != nil { |
| oldMetadata = nil |
| } |
| newMetadata := make([]V2Metadata, 0, len(oldMetadata)) |
| |
| // Copy all other metadata to new slice |
| for _, oldMeta := range oldMetadata { |
| if oldMeta != metadata { |
| newMetadata = append(newMetadata, oldMeta) |
| } |
| } |
| |
| if len(newMetadata) == 0 { |
| return serv.store.Delete(serv.diffIDNamespace(), serv.diffIDKey(diffID)) |
| } |
| |
| jsonBytes, err := json.Marshal(newMetadata) |
| if err != nil { |
| return err |
| } |
| |
| return serv.store.Set(serv.diffIDNamespace(), serv.diffIDKey(diffID), jsonBytes) |
| } |