summaryrefslogtreecommitdiff
path: root/vendor/github.com/Shopify/ejson
diff options
context:
space:
mode:
authorDave Henderson <dhenderson@gmail.com>2018-11-09 23:45:23 -0500
committerDave Henderson <dhenderson@gmail.com>2018-11-09 23:46:55 -0500
commit27078d2f3b249202b8f799d2cf9e0b3cd80d69ec (patch)
treedbd2afd1539294b818b303615e36d850845daf78 /vendor/github.com/Shopify/ejson
parent06955432208ae0c4caf8f2a436af8ba94c97d0ce (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.txt22
-rw-r--r--vendor/github.com/Shopify/ejson/crypto/boxed_message.go104
-rw-r--r--vendor/github.com/Shopify/ejson/crypto/crypto.go160
-rw-r--r--vendor/github.com/Shopify/ejson/ejson.go197
-rw-r--r--vendor/github.com/Shopify/ejson/json/key.go62
-rw-r--r--vendor/github.com/Shopify/ejson/json/pipeline.go73
-rw-r--r--vendor/github.com/Shopify/ejson/json/walker.go133
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
+}