| 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/registry" | |
| "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 *registry.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).Encoded() | |
| } | |
| func (serv *v2MetadataService) digestKey(dgst digest.Digest) string { | |
| return string(dgst.Algorithm()) + "/" + dgst.Encoded() | |
| } | |
| // 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) | |
| } |