package handler import ( "bufio" "bytes" "encoding/hex" "fmt" "io" "net/http" "slices" "strings" "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 trailerHeaders []string trailers map[string]string 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 } if err = c.readCRLF(); err != nil { return num, 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 } // 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 { if len(c.trailerHeaders) != 0 { if err = c.readTrailers(); err != nil { c.err = err return num, c.err } } else if err = c.readCRLF(); err != nil { return num, err } c.err = io.EOF return num, c.err } if err = c.readCRLF(); err != nil { return num, 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 (c *s3v4aChunkReader) readCRLF() error { for _, ch := range [2]byte{'\r', '\n'} { b, err := c.reader.ReadByte() if err == io.EOF { err = io.ErrUnexpectedEOF } if err != nil { c.err = err return c.err } if b != ch { c.err = errMalformedChunkedEncoding return c.err } } return nil } func (c *s3v4aChunkReader) readTrailers() error { var k, v []byte var err error for err == nil { k, err = c.reader.ReadBytes(':') if err != nil { if err == io.EOF { break } c.err = errMalformedTrailerHeaders return c.err } v, err = c.reader.ReadBytes('\n') if err != nil && err != io.EOF { c.err = errMalformedTrailerHeaders return c.err } if len(v) >= 2 && v[len(v)-2] == '\r' { v[len(v)-2] = '\n' v = v[:len(v)-1] } switch { case slices.Contains(c.trailerHeaders, string(k[:len(k)-1])): c.buffer = append(append(c.buffer, k...), v...) // todo use copy case string(k) == "x-amz-trailer-signature:": n, err := hex.Decode(v[:], bytes.TrimRight(v[:], "*\n")) if err != nil { c.err = errMalformedChunkedEncoding return c.err } if err = c.streamSigner.VerifyTrailerSignature(c.buffer, c.requestTime, v[:n]); err != nil { c.err = fmt.Errorf("%w: %s", errs.GetAPIError(errs.ErrSignatureDoesNotMatch), err.Error()) return c.err } default: c.err = errMalformedTrailerHeaders return c.err } c.trailers[string(k[:len(k)-1])] = string(v[:len(v)-1]) } return nil } func (c *s3v4aChunkReader) TrailerHeaders() map[string]string { return c.trailers } func newSignV4aChunkedReader(req *http.Request) (*s3v4aChunkReader, 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) var trailerHeaders []string trailer := req.Header.Get("x-amz-trailer") if trailer != "" { trailerHeaders = strings.Split(trailer, ";") } return &s3v4aChunkReader{ reader: bufio.NewReader(req.Body), streamSigner: newStreamSigner, requestTime: reqTime, buffer: make([]byte, 64*1024), trailerHeaders: trailerHeaders, trailers: make(map[string]string, len(trailerHeaders)), }, nil }