package handler import ( "bufio" "bytes" "encoding/hex" "fmt" "io" "net/http" "time" v4a "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2" errs "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials" ) type ( s3v4aChunkReader struct { reader *bufio.Reader streamSigner *v4a.StreamSigner requestTime time.Time buffer []byte offset int err error } ) func (c *s3v4aChunkReader) Close() (err error) { return nil } func (c *s3v4aChunkReader) Read(buf []byte) (num int, err error) { if c.offset > 0 { num = copy(buf, c.buffer[c.offset:]) if num == len(buf) { c.offset += num return num, nil } c.offset = 0 buf = buf[num:] } var size int for { b, err := c.reader.ReadByte() if err != nil { return c.handleErr(num, err) } if b == ';' { // separating character break } // Manually deserialize the size since AWS specified // the chunk size to be of variable width. In particular, // a size of 16 is encoded as `10` while a size of 64 KB // is `10000`. switch { case b >= '0' && b <= '9': size = size<<4 | int(b-'0') case b >= 'a' && b <= 'f': size = size<<4 | int(b-('a'-10)) case b >= 'A' && b <= 'F': size = size<<4 | int(b-('A'-10)) default: c.err = errMalformedChunkedEncoding return num, c.err } if size > maxChunkSize { c.err = errGiantChunk return num, c.err } } // Now, we read the signature of the following payload and expect: // chunk-signature=" + + "**\r\n" // // The signature is 64 bytes long (hex-encoded SHA256 hash) and // starts with a 16 byte header: len("chunk-signature=") + 144 == 160. var signature [160]byte _, err = io.ReadFull(c.reader, signature[:]) if err != nil { return c.handleErr(num, err) } if !bytes.HasPrefix(signature[:], []byte(chunkSignatureHeader)) { c.err = errMalformedChunkedEncoding return num, c.err } b, err := c.reader.ReadByte() if err != nil { return c.handleErr(num, err) } if b != '\r' { c.err = errMalformedChunkedEncoding return num, c.err } b, err = c.reader.ReadByte() if err != nil { return c.handleErr(num, err) } if b != '\n' { c.err = errMalformedChunkedEncoding return num, c.err } if cap(c.buffer) < size { c.buffer = make([]byte, size) } else { c.buffer = c.buffer[:size] } // Now, we read the payload and compute its SHA-256 hash. _, err = io.ReadFull(c.reader, c.buffer) if err == io.EOF && size != 0 { err = io.ErrUnexpectedEOF } if err != nil && err != io.EOF { c.err = err return num, c.err } b, err = c.reader.ReadByte() if b != '\r' || err != nil { c.err = errMalformedChunkedEncoding return num, c.err } b, err = c.reader.ReadByte() if err != nil { return c.handleErr(num, err) } if b != '\n' { c.err = errMalformedChunkedEncoding return num, c.err } // Once we have read the entire chunk successfully, we verify // that the received signature is valid. n, err := hex.Decode(signature[:], bytes.TrimRight(signature[:], "*")[16:]) if err != nil { c.err = errMalformedChunkedEncoding return num, c.err } if err = c.streamSigner.VerifySignature(nil, c.buffer, c.requestTime, signature[:n]); err != nil { c.err = err return num, c.err } // If the chunk size is zero we return io.EOF. As specified by AWS, // only the last chunk is zero-sized. if size == 0 { c.err = io.EOF return num, c.err } c.offset = copy(buf, c.buffer) num += c.offset return num, err } func (c *s3v4aChunkReader) handleErr(num int, err error) (int, error) { if err == io.EOF { err = io.ErrUnexpectedEOF } c.err = err return num, c.err } func newSignV4aChunkedReader(req *http.Request) (io.ReadCloser, error) { box, err := middleware.GetBoxData(req.Context()) if err != nil { return nil, errs.GetAPIError(errs.ErrAuthorizationHeaderMalformed) } authHeaders, err := middleware.GetAuthHeaders(req.Context()) if err != nil { return nil, errs.GetAPIError(errs.ErrAuthorizationHeaderMalformed) } seed, err := hex.DecodeString(authHeaders.SignatureV4) if err != nil { return nil, errs.GetAPIError(errs.ErrSignatureDoesNotMatch) } reqTime, err := middleware.GetClientTime(req.Context()) if err != nil { return nil, errs.GetAPIError(errs.ErrMalformedDate) } credAdapter := v4a.SymmetricCredentialAdaptor{ SymmetricProvider: credentialsv2.NewStaticCredentialsProvider(authHeaders.AccessKeyID, box.Gate.SecretKey, ""), } creds, err := credAdapter.RetrievePrivateKey(req.Context()) if err != nil { return nil, fmt.Errorf("failed to derive assymetric key from credentials: %w", err) } newStreamSigner := v4a.NewStreamSigner(creds, "s3", seed) return &s3v4aChunkReader{ reader: bufio.NewReader(req.Body), streamSigner: newStreamSigner, requestTime: reqTime, buffer: make([]byte, 64*1024), }, nil }