diff options
| author | Dave Henderson <dhenderson@gmail.com> | 2018-11-09 23:45:23 -0500 |
|---|---|---|
| committer | Dave Henderson <dhenderson@gmail.com> | 2018-11-09 23:46:55 -0500 |
| commit | 27078d2f3b249202b8f799d2cf9e0b3cd80d69ec (patch) | |
| tree | dbd2afd1539294b818b303615e36d850845daf78 /vendor/github.com/Shopify/ejson | |
| parent | 06955432208ae0c4caf8f2a436af8ba94c97d0ce (diff) | |
Vendoring github.com/Shopify/ejson
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
Diffstat (limited to 'vendor/github.com/Shopify/ejson')
| -rw-r--r-- | vendor/github.com/Shopify/ejson/LICENSE.txt | 22 | ||||
| -rw-r--r-- | vendor/github.com/Shopify/ejson/crypto/boxed_message.go | 104 | ||||
| -rw-r--r-- | vendor/github.com/Shopify/ejson/crypto/crypto.go | 160 | ||||
| -rw-r--r-- | vendor/github.com/Shopify/ejson/ejson.go | 197 | ||||
| -rw-r--r-- | vendor/github.com/Shopify/ejson/json/key.go | 62 | ||||
| -rw-r--r-- | vendor/github.com/Shopify/ejson/json/pipeline.go | 73 | ||||
| -rw-r--r-- | vendor/github.com/Shopify/ejson/json/walker.go | 133 |
7 files changed, 751 insertions, 0 deletions
diff --git a/vendor/github.com/Shopify/ejson/LICENSE.txt b/vendor/github.com/Shopify/ejson/LICENSE.txt new file mode 100644 index 00000000..c7a793ba --- /dev/null +++ b/vendor/github.com/Shopify/ejson/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2014 Shopify + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/Shopify/ejson/crypto/boxed_message.go b/vendor/github.com/Shopify/ejson/crypto/boxed_message.go new file mode 100644 index 00000000..5dc86980 --- /dev/null +++ b/vendor/github.com/Shopify/ejson/crypto/boxed_message.go @@ -0,0 +1,104 @@ +package crypto + +import ( + "encoding/base64" + "fmt" + "regexp" + "strconv" +) + +var messageParser = regexp.MustCompile("\\AEJ\\[(\\d):([A-Za-z0-9+=/]{44}):([A-Za-z0-9+=/]{32}):(.+)\\]\\z") + +// boxedMessage dumps and loads the wire format for encrypted messages. The +// schema is fairly simple: +// +// "EJ[" +// SchemaVersion ( "1" ) +// ":" +// EncrypterPublic :: base64-encoded 32-byte key +// ":" +// Nonce :: base64-encoded 24-byte nonce +// ":" +// Box :: base64-encoded encrypted message +// "]" +type boxedMessage struct { + SchemaVersion int + EncrypterPublic [32]byte + Nonce [24]byte + Box []byte +} + +// IsBoxedMessage tests whether a value is formatted using the boxedMessage +// format. This can be used to determine whether a string value requires +// encryption or is already encrypted. +func IsBoxedMessage(data []byte) bool { + return messageParser.Find(data) != nil +} + +// Dump dumps to the wire format +func (b *boxedMessage) Dump() []byte { + pub := base64.StdEncoding.EncodeToString(b.EncrypterPublic[:]) + nonce := base64.StdEncoding.EncodeToString(b.Nonce[:]) + box := base64.StdEncoding.EncodeToString(b.Box) + + str := fmt.Sprintf("EJ[%d:%s:%s:%s]", + b.SchemaVersion, pub, nonce, box) + return []byte(str) +} + +// Load restores from the wire format. +func (b *boxedMessage) Load(from []byte) error { + var ssver, spub, snonce, sbox string + var err error + + allMatches := messageParser.FindAllStringSubmatch(string(from), -1) // -> [][][]byte + if len(allMatches) != 1 { + return fmt.Errorf("invalid message format") + } + matches := allMatches[0] + if len(matches) != 5 { + return fmt.Errorf("invalid message format") + } + + ssver = matches[1] + spub = matches[2] + snonce = matches[3] + sbox = matches[4] + + b.SchemaVersion, err = strconv.Atoi(ssver) + if err != nil { + return err + } + + pub, err := base64.StdEncoding.DecodeString(spub) + if err != nil { + return err + } + pubBytes := []byte(pub) + if len(pubBytes) != 32 { + return fmt.Errorf("public key invalid") + } + var public [32]byte + copy(public[:], pubBytes[0:32]) + b.EncrypterPublic = public + + nnc, err := base64.StdEncoding.DecodeString(snonce) + if err != nil { + return err + } + nonceBytes := []byte(nnc) + if len(nonceBytes) != 24 { + return fmt.Errorf("nonce invalid") + } + var nonce [24]byte + copy(nonce[:], nonceBytes[0:24]) + b.Nonce = nonce + + box, err := base64.StdEncoding.DecodeString(sbox) + if err != nil { + return err + } + b.Box = []byte(box) + + return nil +} diff --git a/vendor/github.com/Shopify/ejson/crypto/crypto.go b/vendor/github.com/Shopify/ejson/crypto/crypto.go new file mode 100644 index 00000000..63de1c6a --- /dev/null +++ b/vendor/github.com/Shopify/ejson/crypto/crypto.go @@ -0,0 +1,160 @@ +// Package crypto implements a simple convenience wrapper around +// golang.org/x/crypto/nacl/box. It ultimately models a situation where you +// don't care about authenticating the encryptor, so the nonce and encryption +// public key are prepended to the encrypted message. +// +// Shared key precomputation is used when encrypting but not when decrypting. +// This is not an inherent limitation, but it would complicate the +// implementation a little bit to do precomputation during decryption also. +// If performance becomes an issue (highly unlikely), it's completely feasible +// to add. +package crypto + +import ( + "crypto/rand" + "errors" + "fmt" + + "golang.org/x/crypto/nacl/box" +) + +// Keypair models a Curve25519 keypair. To generate a new Keypair, declare an +// empty one and call Generate() on it. +type Keypair struct { + Public [32]byte + Private [32]byte +} + +// Encrypter is generated from a keypair (typically a newly-generated ephemeral +// keypair, used only for this session) with the public key of an authorized +// decrypter. It is then capable of encrypting messages to that decrypter's +// private key. An instance should normally be obtained only by calling +// Encrypter() on a Keypair instance. +type Encrypter struct { + Keypair *Keypair + PeerPublic [32]byte + SharedKey [32]byte +} + +// Decrypter is generated from a keypair (a fixed keypair, generally, whose +// private key is stored in configuration management or otherwise), and used to +// decrypt messages. It should normally be obtained by calling Decrypter() on a +// Keypair instance. +type Decrypter struct { + Keypair *Keypair +} + +// ErrDecryptionFailed means the decryption didn't work. This normally +// indicates that the message was corrupted or the wrong keypair was used. +var ErrDecryptionFailed = errors.New("couldn't decrypt message") + +// Generate generates a new Curve25519 keypair into a (presumably) empty Keypair +// structure. +func (k *Keypair) Generate() (err error) { + var pub, priv *[32]byte + pub, priv, err = box.GenerateKey(rand.Reader) + if err != nil { + return + } + k.Public = *pub + k.Private = *priv + return +} + +// PublicString returns the public key in the canonical hex-encoded printable form. +func (k *Keypair) PublicString() string { + return fmt.Sprintf("%x", k.Public) +} + +// PrivateString returns the private key in the canonical hex-encoded printable form. +func (k *Keypair) PrivateString() string { + return fmt.Sprintf("%x", k.Private) +} + +// Encrypter returns an Encrypter instance, given a public key, to encrypt +// messages to the paired, unknown, private key. +func (k *Keypair) Encrypter(peerPublic [32]byte) *Encrypter { + return NewEncrypter(k, peerPublic) +} + +// Decrypter returns a Decrypter instance, used to decrypt properly formatted +// messages from arbitrary encrypters. +func (k *Keypair) Decrypter() *Decrypter { + return &Decrypter{Keypair: k} +} + +// NewEncrypter instantiates an Encrypter after pre-computing the shared key for +// the owned keypair and the given decrypter public key. +func NewEncrypter(kp *Keypair, peerPublic [32]byte) *Encrypter { + var shared [32]byte + box.Precompute(&shared, &peerPublic, &kp.Private) + return &Encrypter{ + Keypair: kp, + PeerPublic: peerPublic, + SharedKey: shared, + } +} + +func (e *Encrypter) encrypt(message []byte) (*boxedMessage, error) { + nonce, err := genNonce() + if err != nil { + return nil, err + } + + out := box.SealAfterPrecomputation(nil, []byte(message), &nonce, &e.SharedKey) + + return &boxedMessage{ + SchemaVersion: 1, + EncrypterPublic: e.Keypair.Public, + Nonce: nonce, + Box: out, + }, nil +} + +// Encrypt takes a plaintext message and returns an encrypted message. Unlike +// raw nacl/box encryption, this message is decryptable without passing the +// nonce or public key out-of-band, as it includes both. This is not less +// secure, it just doesn't allow for authorizing the encryptor. That's fine, +// since authorization isn't a desired property of this particular cryptosystem. +func (e *Encrypter) Encrypt(message []byte) ([]byte, error) { + if IsBoxedMessage(message) { + return message, nil + } + boxedMessage, err := e.encrypt(message) + if err != nil { + return nil, err + } + return boxedMessage.Dump(), nil +} + +// Decrypt is passed an encrypted message or a particular format (the format +// generated by (*Encrypter)Encrypt(), which includes the nonce and public key +// used to create the ciphertext. It returns the decrypted string. Note that, +// unlike with encryption, Shared-key-precomputation is not used for decryption. +func (d *Decrypter) Decrypt(message []byte) ([]byte, error) { + var bm boxedMessage + if err := bm.Load(message); err != nil { + return nil, err + } + return d.decrypt(&bm) +} + +func (d *Decrypter) decrypt(bm *boxedMessage) ([]byte, error) { + plaintext, ok := box.Open(nil, bm.Box, &bm.Nonce, &bm.EncrypterPublic, &d.Keypair.Private) + if !ok { + return nil, ErrDecryptionFailed + } + return plaintext, nil +} + +func genNonce() (nonce [24]byte, err error) { + var n int + n, err = rand.Read(nonce[0:24]) + if err != nil { + return + } + if n != 24 { + err = fmt.Errorf("not enough bytes returned from rand.Reader") + } + return +} diff --git a/vendor/github.com/Shopify/ejson/ejson.go b/vendor/github.com/Shopify/ejson/ejson.go new file mode 100644 index 00000000..b30bca37 --- /dev/null +++ b/vendor/github.com/Shopify/ejson/ejson.go @@ -0,0 +1,197 @@ +// Package ejson implements the primary interface to interact with ejson +// documents and keypairs. The CLI implemented by cmd/ejson is a fairly thin +// wrapper around this package. +package ejson + +import ( + "bytes" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + + "github.com/Shopify/ejson/crypto" + "github.com/Shopify/ejson/json" +) + +// GenerateKeypair is used to create a new ejson keypair. It returns the keys as +// hex-encoded strings, suitable for printing to the screen. hex.DecodeString +// can be used to load the true representation if necessary. +func GenerateKeypair() (pub string, priv string, err error) { + var kp crypto.Keypair + if err := kp.Generate(); err != nil { + return "", "", err + } + return kp.PublicString(), kp.PrivateString(), nil +} + +// Encrypt reads all contents from 'in', extracts the pubkey +// and performs the requested encryption operation, writing +// the resulting data to 'out'. +// Returns the number of bytes written and any error that might have +// occurred. +func Encrypt(in io.Reader, out io.Writer) (int, error) { + data, err := ioutil.ReadAll(in) + if err != nil { + return -1, err + } + + var myKP crypto.Keypair + if err = myKP.Generate(); err != nil { + return -1, err + } + + pubkey, err := json.ExtractPublicKey(data) + if err != nil { + return -1, err + } + + encrypter := myKP.Encrypter(pubkey) + walker := json.Walker{ + Action: encrypter.Encrypt, + } + + newdata, err := walker.Walk(data) + if err != nil { + return -1, err + } + + return out.Write(newdata) +} + +// EncryptFileInPlace takes a path to a file on disk, which must be a valid EJSON file +// (see README.md for more on what constitutes a valid EJSON file). Any +// encryptable-but-unencrypted fields in the file will be encrypted using the +// public key embdded in the file, and the resulting text will be written over +// the file present on disk. +func EncryptFileInPlace(filePath string) (int, error) { + var fileMode os.FileMode + if stat, err := os.Stat(filePath); err == nil { + fileMode = stat.Mode() + } else { + return -1, err + } + + file, err := os.Open(filePath) + if err != nil { + return -1, err + } + + var outBuffer bytes.Buffer + + written, err := Encrypt(file, &outBuffer) + if err != nil { + return -1, err + } + + if err = file.Close(); err != nil { + return -1, err + } + + if err := ioutil.WriteFile(filePath, outBuffer.Bytes(), fileMode); err != nil { + return -1, err + } + + return written, nil +} + +// Decrypt reads an ejson stream from 'in' and writes the decrypted data to 'out'. +// The private key is expected to be under 'keydir'. +// Returns error upon failure, or nil on success. +func Decrypt(in io.Reader, out io.Writer, keydir string, userSuppliedPrivateKey string) error { + data, err := ioutil.ReadAll(in) + if err != nil { + return err + } + + pubkey, err := json.ExtractPublicKey(data) + if err != nil { + return err + } + + privkey, err := findPrivateKey(pubkey, keydir, userSuppliedPrivateKey) + if err != nil { + return err + } + + myKP := crypto.Keypair{ + Public: pubkey, + Private: privkey, + } + + decrypter := myKP.Decrypter() + walker := json.Walker{ + Action: decrypter.Decrypt, + } + + newdata, err := walker.Walk(data) + if err != nil { + return err + } + + _, err = out.Write(newdata) + + return err +} + +// DecryptFile takes a path to an encrypted EJSON file and returns the data +// decrypted. The public key used to encrypt the values is embedded in the +// referenced document, and the matching private key is searched for in keydir. +// There must exist a file in keydir whose name is the public key from the +// EJSON document, and whose contents are the corresponding private key. See +// README.md for more details on this. +func DecryptFile(filePath, keydir string, userSuppliedPrivateKey string) ([]byte, error) { + if _, err := os.Stat(filePath); err != nil { + return nil, err + } + + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + var outBuffer bytes.Buffer + + err = Decrypt(file, &outBuffer, keydir, userSuppliedPrivateKey) + + return outBuffer.Bytes(), err +} + +func readPrivateKeyFromDisk(pubkey [32]byte, keydir string) (privkey string, err error) { + keyFile := fmt.Sprintf("%s/%x", keydir, pubkey) + var fileContents []byte + fileContents, err = ioutil.ReadFile(keyFile) + if err != nil { + err = fmt.Errorf("couldn't read key file (%s)", err.Error()) + return + } + privkey = string(fileContents) + return +} + +func findPrivateKey(pubkey [32]byte, keydir string, userSuppliedPrivateKey string) (privkey [32]byte, err error) { + var privkeyString string + if userSuppliedPrivateKey != "" { + privkeyString = userSuppliedPrivateKey + } else { + privkeyString, err = readPrivateKeyFromDisk(pubkey, keydir) + if err != nil { + return privkey, err + } + } + + privkeyBytes, err := hex.DecodeString(strings.TrimSpace(privkeyString)) + if err != nil { + return + } + + if len(privkeyBytes) != 32 { + err = fmt.Errorf("invalid private key") + return + } + copy(privkey[:], privkeyBytes) + return +} diff --git a/vendor/github.com/Shopify/ejson/json/key.go b/vendor/github.com/Shopify/ejson/json/key.go new file mode 100644 index 00000000..66177c88 --- /dev/null +++ b/vendor/github.com/Shopify/ejson/json/key.go @@ -0,0 +1,62 @@ +package json + +import ( + "encoding/hex" + "encoding/json" + "errors" +) + +const ( + // PublicKeyField is the key name at which the public key should be + // stored in an EJSON document. + PublicKeyField = "_public_key" +) + +// ErrPublicKeyMissing indicates that the PublicKeyField key was not found +// at the top level of the JSON document provided. +var ErrPublicKeyMissing = errors.New("public key not present in EJSON file") + +// ErrPublicKeyInvalid means that the PublicKeyField key was found, but the +// value could not be parsed into a valid key. +var ErrPublicKeyInvalid = errors.New("public key has invalid format") + +// ExtractPublicKey finds the _public_key value in an EJSON document and +// parses it into a key usable with the crypto library. +func ExtractPublicKey(data []byte) (key [32]byte, err error) { + var ( + obj map[string]interface{} + ks string + ok bool + bs []byte + ) + err = json.Unmarshal(data, &obj) + if err != nil { + return + } + k, ok := obj[PublicKeyField] + if !ok { + goto missing + } + ks, ok = k.(string) + if !ok { + goto invalid + } + if len(ks) != 64 { + goto invalid + } + bs, err = hex.DecodeString(ks) + if err != nil { + goto invalid + } + if len(bs) != 32 { + goto invalid + } + copy(key[:], bs) + return +missing: + err = ErrPublicKeyMissing + return +invalid: + err = ErrPublicKeyInvalid + return +} diff --git a/vendor/github.com/Shopify/ejson/json/pipeline.go b/vendor/github.com/Shopify/ejson/json/pipeline.go new file mode 100644 index 00000000..4527a18d --- /dev/null +++ b/vendor/github.com/Shopify/ejson/json/pipeline.go @@ -0,0 +1,73 @@ +package json + +type pipeline struct { + final []byte + err error + pendingBytes []byte + queue chan queueItem + done chan struct{} +} + +type queueItem struct { + pr <-chan promiseResult + bs []byte + term bool +} + +type promiseResult struct { + bytes []byte + err error +} + +func newPipeline() *pipeline { + pl := &pipeline{ + queue: make(chan queueItem, 512), + done: make(chan struct{}), + } + go pl.run() + return pl +} + +func (p *pipeline) run() { + for qi := range p.queue { + if qi.term { + close(p.done) + } else if qi.pr != nil { + res := <-qi.pr + if res.err != nil { + p.err = res.err + } + p.final = append(p.final, res.bytes...) + } else { + p.final = append(p.final, qi.bs...) + } + } +} + +func (p *pipeline) appendBytes(bs []byte) { + p.pendingBytes = append(p.pendingBytes, bs...) +} + +func (p *pipeline) appendByte(b byte) { + p.pendingBytes = append(p.pendingBytes, b) +} + +func (p *pipeline) appendPromise(ch <-chan promiseResult) { + p.flushPendingBytes() + p.queue <- queueItem{pr: ch} +} + +func (p *pipeline) flush() ([]byte, error) { + p.flushPendingBytes() + p.queue <- queueItem{term: true} + <-p.done + close(p.queue) + return p.final, p.err +} + +func (p *pipeline) flushPendingBytes() { + if len(p.pendingBytes) > 0 { + p.queue <- queueItem{bs: p.pendingBytes} + p.pendingBytes = nil + } +} diff --git a/vendor/github.com/Shopify/ejson/json/walker.go b/vendor/github.com/Shopify/ejson/json/walker.go new file mode 100644 index 00000000..b377f67e --- /dev/null +++ b/vendor/github.com/Shopify/ejson/json/walker.go @@ -0,0 +1,133 @@ +// Package json implements functions to load the Public key data from an EJSON +// file, and to walk that data file, encrypting or decrypting any keys which, +// according to the specification, are marked as encryptable (see README.md for +// details). +// +// It may be non-obvious why this is implemented using a scanner and not by +// loading the structure, manipulating it, then dumping it. Since Go's maps are +// explicitly randomized, that would cause the entire structure to be randomized +// each time the file was written, rendering diffs over time essentially +// useless. +package json + +import ( + "bytes" + "fmt" + + "github.com/dustin/gojson" +) + +// Walker takes an Action, which will run on fields selected by EJSON for +// encryption, and provides a Walk method, which iterates on all the fields in +// a JSON text, running the Action on all selected fields. Fields are selected +// if they are a Value (not a Key) of type string, and their referencing Key did +// *not* begin with an Underscore. Note that this +// underscore-to-disable-encryption syntax does not propagate down the hierarchy +// to children. +// That is: +// * In {"_a": "b"}, Action will not be run at all. +// * In {"a": "b"}, Action will be run with "b", and the return value will +// replace "b". +// * In {"k": {"a": ["b"]}, Action will run on "b". +// * In {"_k": {"a": ["b"]}, Action run on "b". +// * In {"k": {"_a": ["b"]}, Action will not run. +type Walker struct { + Action func([]byte) ([]byte, error) +} + +// Walk walks an entire JSON structure, running the ejsonWalker.Action on each +// actionable node. A node is actionable if it's a string *value*, and its +// referencing key doesn't begin with an underscore. For each actionable node, +// the contents are replaced with the result of Action. Everything else is +// unchanged. +func (ew *Walker) Walk(data []byte) ([]byte, error) { + var ( + inLiteral bool + literalStart int + isComment bool + scanner json.Scanner + ) + scanner.Reset() + pline := newPipeline() + for i, c := range data { + switch v := scanner.Step(&scanner, int(c)); v { + case json.ScanContinue, json.ScanSkipSpace: + // Uninteresting byte. Just advance to next. + case json.ScanBeginLiteral: + inLiteral = true + literalStart = i + case json.ScanObjectKey: + // The literal we just finished reading was a Key. Decide whether it was a + // encryptable by checking whether the first byte after the '"' was an + // underscore, then append it verbatim to the output buffer. + inLiteral = false + isComment = data[literalStart+1] == '_' + pline.appendBytes(data[literalStart:i]) + case json.ScanError: + // Some error happened; just bail. + pline.flush() + return nil, fmt.Errorf("invalid json") + case json.ScanEnd: + // We successfully hit the end of input. + pline.appendByte(c) + return pline.flush() + default: + if inLiteral { + inLiteral = false + // We finished reading some literal, and it wasn't a Key, meaning it's + // potentially encryptable. If it was a string, and the most recent Key + // encountered didn't begin with a '_', we are to encrypt it. In any + // other case, we append it verbatim to the output buffer. + if isComment || data[literalStart] != '"' { + pline.appendBytes(data[literalStart:i]) + } else { + res := make(chan promiseResult) + go func(subData []byte) { + actioned, err := ew.runAction(subData) + res <- promiseResult{actioned, err} + close(res) + }(data[literalStart:i]) + pline.appendPromise(res) + } + } + } + if !inLiteral { + // If we're in a literal, we save up bytes because we may have to encrypt + // them. Outside of a literal, we simply append each byte as we read it. + pline.appendByte(c) + } + } + if scanner.EOF() == json.ScanError { + // Unexpected EOF => malformed JSON + pline.flush() + return nil, fmt.Errorf("invalid json") + } + return pline.flush() +} + +func (ew *Walker) runAction(data []byte) ([]byte, error) { + trimmed := bytes.TrimSpace(data) + unquoted, ok := json.UnquoteBytes(trimmed) + if !ok { + return nil, fmt.Errorf("invalid json") + } + done, err := ew.Action(unquoted) + if err != nil { + return nil, err + } + quoted, err := quoteBytes(done) + if err != nil { + return nil, err + } + return append(quoted, data[len(trimmed):]...), nil +} + +// probably a better way to do this, but... +func quoteBytes(in []byte) ([]byte, error) { + data := []string{string(in)} + out, err := json.Marshal(data) + if err != nil { + return nil, err + } + return out[1 : len(out)-1], nil +} |
