// Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package kms import ( "bytes" "sort" "unicode/utf8" ) // Context is a set of key-value pairs that // are associated with a generate data encryption // key (DEK). // // A KMS implementation may bind the context to the // generated DEK such that the same context must be // provided when decrypting an encrypted DEK. type Context map[string]string // MarshalText returns a canonical text representation of // the Context. // MarshalText sorts the context keys and writes the sorted // key-value pairs as canonical JSON object. The sort order // is based on the un-escaped keys. It never returns an error. func (c Context) MarshalText() ([]byte, error) { if len(c) == 0 { return []byte{'{', '}'}, nil } // Pre-allocate a buffer - 128 bytes is an arbitrary // heuristic value that seems like a good starting size. var b = bytes.NewBuffer(make([]byte, 0, 128)) if len(c) == 1 { for k, v := range c { b.WriteString(`{"`) escapeStringJSON(b, k) b.WriteString(`":"`) escapeStringJSON(b, v) b.WriteString(`"}`) } return b.Bytes(), nil } sortedKeys := make([]string, 0, len(c)) for k := range c { sortedKeys = append(sortedKeys, k) } sort.Strings(sortedKeys) b.WriteByte('{') for i, k := range sortedKeys { b.WriteByte('"') escapeStringJSON(b, k) b.WriteString(`":"`) escapeStringJSON(b, c[k]) b.WriteByte('"') if i < len(sortedKeys)-1 { b.WriteByte(',') } } b.WriteByte('}') return b.Bytes(), nil } // Adapted from Go stdlib. var hexTable = "0123456789abcdef" // escapeStringJSON will escape a string for JSON and write it to dst. func escapeStringJSON(dst *bytes.Buffer, s string) { start := 0 for i := 0; i < len(s); { if b := s[i]; b < utf8.RuneSelf { if htmlSafeSet[b] { i++ continue } if start < i { dst.WriteString(s[start:i]) } dst.WriteByte('\\') switch b { case '\\', '"': dst.WriteByte(b) case '\n': dst.WriteByte('n') case '\r': dst.WriteByte('r') case '\t': dst.WriteByte('t') default: // This encodes bytes < 0x20 except for \t, \n and \r. // If escapeHTML is set, it also escapes <, >, and & // because they can lead to security holes when // user-controlled strings are rendered into JSON // and served to some browsers. dst.WriteString(`u00`) dst.WriteByte(hexTable[b>>4]) dst.WriteByte(hexTable[b&0xF]) } i++ start = i continue } c, size := utf8.DecodeRuneInString(s[i:]) if c == utf8.RuneError && size == 1 { if start < i { dst.WriteString(s[start:i]) } dst.WriteString(`\ufffd`) i += size start = i continue } // U+2028 is LINE SEPARATOR. // U+2029 is PARAGRAPH SEPARATOR. // They are both technically valid characters in JSON strings, // but don't work in JSONP, which has to be evaluated as JavaScript, // and can lead to security holes there. It is valid JSON to // escape them, so we do so unconditionally. // See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion. if c == '\u2028' || c == '\u2029' { if start < i { dst.WriteString(s[start:i]) } dst.WriteString(`\u202`) dst.WriteByte(hexTable[c&0xF]) i += size start = i continue } i += size } if start < len(s) { dst.WriteString(s[start:]) } } // htmlSafeSet holds the value true if the ASCII character with the given // array position can be safely represented inside a JSON string, embedded // inside of HTML