| // Copyright 2018 The LUCI Authors. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package common |
| |
| import ( |
| "encoding/base64" |
| "encoding/hex" |
| "fmt" |
| |
| "go.chromium.org/luci/common/errors" |
| "go.chromium.org/luci/grpc/grpcutil" |
| |
| api "go.chromium.org/luci/cipd/api/cipd/v1" |
| ) |
| |
| // HashAlgoValidation is passed to ValidateObjectRef. |
| type HashAlgoValidation bool |
| |
| const ( |
| // KnownHash indicates that ValidateObjectRef should only accept refs that |
| // use hash algo known to the current version of the code. |
| // |
| // This is primarily useful on the backend, since it always needs to be sure |
| // that it can handle any ObjectRef that is passed to it. |
| KnownHash HashAlgoValidation = true |
| |
| // AnyHash indicates that ValidateObject may accept refs that use algo number |
| // not known to the current version of the code (i.e. presumably from the |
| // future version of the protocol). |
| // |
| // This is useful on the client that for some operations just round-trips |
| // ObjectRef it received from the server, without trying to interpret it. |
| // Using AnyHash allows the client to be friendlier to future protocol |
| // changes. This is particularly important in self-update flow. |
| AnyHash HashAlgoValidation = false |
| ) |
| |
| // ValidateInstanceID returns an error if the string isn't a valid instance id. |
| // |
| // If v is KnownHash, will verify the current version of the code understands |
| // the hash algo encoded in idd. Otherwise (if v is AnyHash) will just validate |
| // that iid looks syntactically sane, and will accept any hash algo (even if the |
| // current code doesn't understand it). This is useful when round-tripping |
| // ObjectRefs and instance IDs through the (outdated) client code back to the |
| // server. |
| func ValidateInstanceID(iid string, v HashAlgoValidation) (err error) { |
| if len(iid) == 40 { |
| // Legacy SHA1-based instances use hex(sha1) as instance ID, 40 chars. |
| err = checkIsHex(iid) |
| } else { |
| var ref *api.ObjectRef |
| ref, err = decodeObjectRef(iid) |
| if err == nil && v == KnownHash { |
| err = ValidateObjectRef(ref, KnownHash) |
| } |
| } |
| if err == nil { |
| return |
| } |
| return fmt.Errorf("not a valid package instance ID %q: %s", iid, err) |
| } |
| |
| // ValidateObjectRef returns a grpc-annotated error if the given object ref is |
| // invalid. |
| // |
| // If v is KnownHash, will verify the current version of the code understands |
| // the hash algo. Otherwise (if v is AnyHash) will just validate that the hex |
| // digest looks sane, and will accept any hash algo (even if the current code |
| // doesn't understand it). This is useful when round-tripping ObjectRefs and |
| // instance IDs through the (outdated) client code back to the server. |
| // |
| // Errors have InvalidArgument grpc code. |
| func ValidateObjectRef(ref *api.ObjectRef, v HashAlgoValidation) error { |
| if ref == nil { |
| return errors.Reason("the object ref is not provided"). |
| Tag(grpcutil.InvalidArgumentTag).Err() |
| } |
| |
| switch { |
| case ref.HashAlgo < 0: |
| return errors.Reason("bad negative hash algo").Tag(grpcutil.InvalidArgumentTag).Err() |
| case ref.HashAlgo == 0: |
| return errors.Reason("unspecified hash algo").Tag(grpcutil.InvalidArgumentTag).Err() |
| } |
| |
| if err := checkIsHex(ref.HexDigest); err != nil { |
| return errors.Annotate(err, "invalid %s hex digest", ref.HashAlgo). |
| Tag(grpcutil.InvalidArgumentTag).Err() |
| } |
| |
| if v { |
| if err := ValidateHashAlgo(ref.HashAlgo); err != nil { |
| return err |
| } |
| hexDigestLen := supportedAlgos[ref.HashAlgo].hexDigestLen |
| if len(ref.HexDigest) != hexDigestLen { |
| return errors.Reason("invalid %s digest: expecting %d chars, got %d", ref.HashAlgo, hexDigestLen, len(ref.HexDigest)). |
| Tag(grpcutil.InvalidArgumentTag).Err() |
| } |
| } |
| |
| return nil |
| } |
| |
| // ObjectRefToInstanceID returns an Instance ID that matches the given CAS |
| // object ref. |
| // |
| // Instance ID is a human readable string representation of ObjectRef used in |
| // higher level APIs (command line flags, ensure files, etc) and internally in |
| // the datastore. |
| // |
| // Its exact form depends on a hash being used: |
| // * SHA1: Instance IDs are hex encoded SHA1 digests, in lowercase. |
| // * Everything else: base64(digest + []byte{ref.HashAlgo}), where base64 is |
| // without padding and using URL-safe charset. |
| // |
| // The ref is not checked for correctness. Use ValidateObjectRef if this is |
| // a concern. Panics if something is not right. |
| func ObjectRefToInstanceID(ref *api.ObjectRef) string { |
| if err := ValidateObjectRef(ref, AnyHash); err != nil { |
| panic(err) |
| } |
| if ref.HashAlgo == api.HashAlgo_SHA1 { |
| return ref.HexDigest // legacy SHA1 instance ID format |
| } |
| return encodeObjectRef(ref) |
| } |
| |
| // InstanceIDToObjectRef is a reverse of ObjectRefToInstanceID. |
| // |
| // Panics if the instance ID is incorrect. Use ValidateInstanceID if |
| // this is a concern. |
| func InstanceIDToObjectRef(iid string) *api.ObjectRef { |
| // Legacy SHA1-based instances use hex(sha1) as instance ID, 40 chars. |
| if len(iid) == 40 { |
| if err := checkIsHex(iid); err != nil { |
| panic(fmt.Errorf("not a valid package instance ID %q: %s", iid, err)) |
| } |
| return &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: iid, |
| } |
| } |
| ref, err := decodeObjectRef(iid) |
| if err != nil { |
| panic(fmt.Errorf("not a valid package instance ID %q: %s", iid, err)) |
| } |
| return ref |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| // encodeObjectRef returns a compact stable human-readable serialization of |
| // ObjectRef. |
| // |
| // It takes the digest blob, appends a byte with ref.HashAlgo value to it, and |
| // encodes the resulting blob using raw URL-safe base64 encoding. |
| // |
| // Panics if ref is invalid. |
| func encodeObjectRef(ref *api.ObjectRef) string { |
| switch { |
| case ref.HashAlgo < 0: |
| panic(fmt.Errorf("bad negative hash algo %d", ref.HashAlgo)) |
| case ref.HashAlgo == 0: |
| panic(fmt.Errorf("unspecified hash algo")) |
| } |
| |
| // If algo is known to us, make sure it is valid. Otherwise just encode what |
| // we've given, assuming the consumer will eventually understand it. |
| if int(ref.HashAlgo) < len(supportedAlgos) { |
| if prop := supportedAlgos[ref.HashAlgo]; len(ref.HexDigest) != prop.hexDigestLen { |
| panic(fmt.Errorf("wrong hex digest len %d for algo %s", len(ref.HexDigest), ref.HashAlgo)) |
| } |
| } |
| |
| blob, err := hex.DecodeString(ref.HexDigest) |
| if err != nil { |
| panic(fmt.Errorf("bad hex digest %q: %s", ref.HexDigest, err)) |
| } |
| blob = append(blob, byte(ref.HashAlgo)) |
| return base64.RawURLEncoding.EncodeToString(blob) |
| } |
| |
| // decodeObjectRef is a reverse of encodeObjectRef. |
| func decodeObjectRef(iid string) (*api.ObjectRef, error) { |
| // Skip obviously wrong instance IDs faster and with a cleaner error message. |
| // We assume we use at least 160 bit digests here (which translates to at |
| // least 28 bytes of encoded iid). |
| if len(iid) < 28 { |
| return nil, fmt.Errorf("not a valid size for an encoded digest") |
| } |
| |
| blob, err := base64.RawURLEncoding.DecodeString(iid) |
| switch { |
| case err != nil: |
| return nil, fmt.Errorf("cannot base64 decode: %s", err) |
| case len(blob) == 0: |
| return nil, fmt.Errorf("empty") |
| case len(blob)%2 != 1: // 1 byte for hashAlgo, the rest is the digest |
| return nil, fmt.Errorf("the digest can't be odd") |
| } |
| |
| hashAlgo := api.HashAlgo(blob[len(blob)-1]) |
| digest := blob[:len(blob)-1] |
| |
| if hashAlgo == 0 { |
| return nil, fmt.Errorf("unspecified hash algo (0)") |
| } |
| |
| // If algo is known to us, make sure it is valid. Otherwise just decode what |
| // we've given, assuming the caller will later verify the hash using |
| // ValidateHashAlgo, if really needed. |
| if int(hashAlgo) < len(supportedAlgos) { |
| if prop := supportedAlgos[hashAlgo]; len(digest)*2 != prop.hexDigestLen { |
| return nil, fmt.Errorf("wrong digest len %d for algo %s", len(digest), hashAlgo) |
| } |
| } |
| |
| return &api.ObjectRef{ |
| HashAlgo: hashAlgo, |
| HexDigest: hex.EncodeToString(digest), |
| }, nil |
| } |
| |
| // checkIsHex returns an error if a string is not a lowercase hex string. |
| // |
| // Empty string is rejected as invalid too. |
| func checkIsHex(s string) error { |
| switch { |
| case s == "": |
| return fmt.Errorf("empty hex string") |
| case len(s)%2 != 0: |
| return fmt.Errorf("uneven number of symbols in the hex string") |
| } |
| for _, c := range s { |
| if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) { |
| return fmt.Errorf("bad lowercase hex string %q, wrong char %c", s, c) |
| } |
| } |
| return nil |
| } |