Skip to content

Commit f814595

Browse files
authored
Merge pull request #127 from Shopify/collapse-newlines
2 parents ba85112 + f0c681a commit f814595

File tree

6 files changed

+101
-10
lines changed

6 files changed

+101
-10
lines changed

dev.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ up:
55
- gnu-tar
66
- ruby: 3.0.3
77
- go:
8-
version: 1.18
8+
version: '1.20'
99
- bundler
1010

1111
commands:

ejson.go

+5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ func Encrypt(in io.Reader, out io.Writer) (int, error) {
4343
return -1, err
4444
}
4545

46+
data, err = json.CollapseMultilineStringLiterals(data)
47+
if err != nil {
48+
return -1, err
49+
}
50+
4651
pubkey, err := json.ExtractPublicKey(data)
4752
if err != nil {
4853
return -1, err

ejson_test.go

+14-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func TestEncryptFileInPlace(t *testing.T) {
6969
_, err := EncryptFileInPlace(tempFileName)
7070
Convey("should fail", func() {
7171
So(err, ShouldNotBeNil)
72-
So(err.Error(), ShouldContainSubstring, "invalid character")
72+
So(err.Error(), ShouldContainSubstring, "invalid json")
7373
})
7474
})
7575

@@ -95,6 +95,19 @@ func TestEncryptFileInPlace(t *testing.T) {
9595
})
9696
})
9797

98+
Convey("called with a valid keypair and multiline string", func() {
99+
setData(tempFileName, []byte(`{"_public_key": "`+validPubKey+"\", \"a\": \"b\nc\"\n}"))
100+
101+
_, err := EncryptFileInPlace(tempFileName)
102+
output, err := ioutil.ReadFile(tempFileName)
103+
So(err, ShouldBeNil)
104+
Convey("should encrypt the file", func() {
105+
So(err, ShouldBeNil)
106+
match := regexp.MustCompile(`{"_public_key": "8d8.*", "a": "EJ.*"`+"\n}")
107+
So(match.Find(output), ShouldNotBeNil)
108+
})
109+
})
110+
98111
})
99112
}
100113

json/walker.go

+60-6
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,70 @@ import (
2525
// underscore-to-disable-encryption syntax does not propagate down the hierarchy
2626
// to children.
2727
// That is:
28-
// * In {"_a": "b"}, Action will not be run at all.
29-
// * In {"a": "b"}, Action will be run with "b", and the return value will
30-
// replace "b".
31-
// * In {"k": {"a": ["b"]}, Action will run on "b".
32-
// * In {"_k": {"a": ["b"]}, Action run on "b".
33-
// * In {"k": {"_a": ["b"]}, Action will not run.
28+
// - In {"_a": "b"}, Action will not be run at all.
29+
// - In {"a": "b"}, Action will be run with "b", and the return value will
30+
// replace "b".
31+
// - In {"k": {"a": ["b"]}, Action will run on "b".
32+
// - In {"_k": {"a": ["b"]}, Action run on "b".
33+
// - In {"k": {"_a": ["b"]}, Action will not run.
3434
type Walker struct {
3535
Action func([]byte) ([]byte, error)
3636
}
3737

38+
// It's common to want to paste multiline secrets into an EJSON file, and JSON
39+
// doesn't handle multiline literals, so we cheat here. Our first pass over the
40+
// file is to replace embedded newlines in string literals with escaped newlines.
41+
func CollapseMultilineStringLiterals(data []byte) ([]byte, error) {
42+
var (
43+
inString bool
44+
esc bool
45+
scanner json.Scanner
46+
buf = make([]byte, 0, len(data))
47+
)
48+
49+
scanner.Reset()
50+
for _, c := range data {
51+
if inString && c == '\n' {
52+
buf = append(buf, []byte{'\\', 'n'}...)
53+
continue
54+
} else if inString && c == '\r' {
55+
buf = append(buf, []byte{'\\', 'r'}...)
56+
continue
57+
}
58+
buf = append(buf, c)
59+
switch v := scanner.Step(&scanner, int(c)); v {
60+
case json.ScanContinue:
61+
switch c {
62+
case '\\':
63+
esc = !esc
64+
case '"':
65+
if esc {
66+
esc = false
67+
} else {
68+
inString = false
69+
}
70+
default:
71+
esc = false
72+
}
73+
case json.ScanBeginLiteral:
74+
esc = false
75+
inString = (c == '"')
76+
case json.ScanError:
77+
return nil, fmt.Errorf("invalid json")
78+
case json.ScanEnd:
79+
return buf, nil
80+
default:
81+
inString = false
82+
esc = false
83+
}
84+
}
85+
if scanner.EOF() == json.ScanError {
86+
// Unexpected EOF => malformed JSON
87+
return nil, fmt.Errorf("invalid json")
88+
}
89+
return buf, nil
90+
}
91+
3892
// Walk walks an entire JSON structure, running the ejsonWalker.Action on each
3993
// actionable node. A node is actionable if it's a string *value*, and its
4094
// referencing key doesn't begin with an underscore. For each actionable node,

json/walker_test.go

+17-2
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,29 @@ func TestWalker(t *testing.T) {
1212
}
1313

1414
Convey("Walker passes the provided test-cases", t, func() {
15-
for _, tc := range testCases {
15+
for _, tc := range walkTestCases {
1616
walker := Walker{Action: action}
1717
act, err := walker.Walk([]byte(tc.in))
1818
So(err, ShouldBeNil)
1919
So(string(act), ShouldEqual, tc.out)
2020
}
2121
})
22+
23+
Convey("CollapseMultilineStringLiterals passes the provided test-cases", t, func() {
24+
for _, tc := range collapseTestCases {
25+
act, err := CollapseMultilineStringLiterals([]byte(tc.in))
26+
So(err, ShouldBeNil)
27+
So(string(act), ShouldEqual, tc.out)
28+
}
29+
})
2230
}
2331

2432
type testCase struct {
2533
in, out string
2634
}
2735

2836
// "E" means encrypted.
29-
var testCases = []testCase{
37+
var walkTestCases = []testCase{
3038
{`{"a": "b"}`, `{"a": "E"}`}, // encryption
3139
{`{"a" : "b"}`, `{"a" : "E"}`}, // weird spacing
3240
{` { "a" :"b" } `, ` { "a" :"E" } `}, // trailing spaces
@@ -41,3 +49,10 @@ var testCases = []testCase{
4149
{`{"a": {"_b": "c"}}`, `{"a": {"_b": "c"}}`}, // nested comment
4250
{`{"_a": {"b": "c"}}`, `{"_a": {"b": "E"}}`}, // comments don't inherit
4351
}
52+
53+
var collapseTestCases = []testCase{
54+
{
55+
"{\"a\": \"b\r\nc\nd\"\r\n}",
56+
"{\"a\": \"b\\r\\nc\\nd\"\r\n}",
57+
},
58+
}

man/man5/ejson.5.ronn

+4
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ example, the password in this excerpt `will` be encrypted.
5050
}
5151
```
5252

53+
For convenience, JSON strings to be encrypted are allowed to contain embedded
54+
newline characters. These will be translated to escaped newlines. This is often
55+
useful when encrypting multi-line secrets such as PEM-encoded certificates.
56+
5357
## SECRET SCHEMA
5458

5559
When a value is encrypted, it will be replaced by a relatively long string of

0 commit comments

Comments
 (0)