Skip to content

Commit c39fa65

Browse files
authored
Merge pull request #115 from smallstep/base64
Add command for base64 encoding/decoding
2 parents 4111ddf + 592fd53 commit c39fa65

File tree

5 files changed

+270
-21
lines changed

5 files changed

+270
-21
lines changed

cmd/step/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/smallstep/cli/usage"
2020

2121
// Enabled commands
22+
_ "github.com/smallstep/cli/command/base64"
2223
_ "github.com/smallstep/cli/command/ca"
2324
_ "github.com/smallstep/cli/command/certificate"
2425
_ "github.com/smallstep/cli/command/crypto"

command/base64/base64.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package base64
2+
3+
import (
4+
"bytes"
5+
"encoding/base64"
6+
"fmt"
7+
"os"
8+
"strings"
9+
10+
"github.com/pkg/errors"
11+
"github.com/smallstep/cli/command"
12+
"github.com/smallstep/cli/utils"
13+
"github.com/urfave/cli"
14+
)
15+
16+
func init() {
17+
cmd := cli.Command{
18+
Name: "base64",
19+
Action: command.ActionFunc(base64Action),
20+
Usage: "encodes and decodes using base64 representation",
21+
UsageText: `**step base64** [**-d**|**--decode**] [**-r**|**--raw**] [**-u**|**--url**]`,
22+
Description: `**step base64** implements base64 encoding as specified by RFC 4648.
23+
24+
## Examples
25+
26+
Encode to base64 using the standard encoding:
27+
'''
28+
$ echo -n This is the string to encode | step base64
29+
VGhpcyBpcyB0aGUgc3RyaW5nIHRvIGVuY29kZQ==
30+
$ step base64 This is the string to encode
31+
VGhpcyBpcyB0aGUgc3RyaW5nIHRvIGVuY29kZQ==
32+
'''
33+
34+
Decode a base64 encoded string:
35+
'''
36+
$ echo VGhpcyBpcyB0aGUgc3RyaW5nIHRvIGVuY29kZQ== | step base64 -d
37+
This is the string to encode
38+
'''
39+
40+
Encode to base64 without padding:
41+
'''
42+
$ echo -n This is the string to encode | step base64 -r
43+
VGhpcyBpcyB0aGUgc3RyaW5nIHRvIGVuY29kZQ
44+
$ step base64 -r This is the string to encode
45+
VGhpcyBpcyB0aGUgc3RyaW5nIHRvIGVuY29kZQ
46+
'''
47+
48+
Encode to base64 using the url encoding:
49+
'''
50+
$ echo 'abc123$%^&*()_+-=~' | step base64 -u
51+
YWJjMTIzJCVeJiooKV8rLT1-Cg==
52+
'''
53+
54+
Decode an url encoded base64 string. The encoding type can be enforced
55+
using the '-u' or '-r' flags, but it will be autodetected if they are not
56+
passed:
57+
'''
58+
$ echo YWJjMTIzJCVeJiooKV8rLT1-Cg== | step base64 -d
59+
abc123$%^&*()_+-=~
60+
$ echo YWJjMTIzJCVeJiooKV8rLT1-Cg== | step base64 -d -u
61+
abc123$%^&*()_+-=~
62+
'''`,
63+
Flags: []cli.Flag{
64+
cli.BoolFlag{
65+
Name: "d,decode",
66+
Usage: "decode base64 input",
67+
},
68+
cli.BoolFlag{
69+
Name: "r,raw",
70+
Usage: "use the unpadded base64 encoding",
71+
},
72+
cli.BoolFlag{
73+
Name: "u,url",
74+
Usage: "use the encoding format typically used in URLs and file names",
75+
},
76+
},
77+
}
78+
79+
command.Register(cmd)
80+
}
81+
82+
func base64Action(ctx *cli.Context) error {
83+
var err error
84+
var data []byte
85+
isDecode := ctx.Bool("decode")
86+
87+
if ctx.NArg() > 0 {
88+
data = []byte(strings.Join(ctx.Args(), " "))
89+
} else {
90+
var prompt string
91+
if isDecode {
92+
prompt = "Please enter text to decode"
93+
} else {
94+
prompt = "Please enter text to encode"
95+
}
96+
97+
if data, err = utils.ReadInput(prompt); err != nil {
98+
return err
99+
}
100+
}
101+
102+
enc := getEncoder(ctx, data)
103+
if isDecode {
104+
b, err := enc.DecodeString(string(data))
105+
if err != nil {
106+
return errors.Wrap(err, "error decoding input")
107+
}
108+
os.Stdout.Write(b)
109+
} else {
110+
fmt.Println(enc.EncodeToString(data))
111+
}
112+
113+
return nil
114+
}
115+
116+
func getEncoder(ctx *cli.Context, data []byte) *base64.Encoding {
117+
raw := ctx.Bool("raw")
118+
url := ctx.Bool("url")
119+
isDecode := ctx.Bool("decode")
120+
121+
// Detect encoding
122+
if isDecode && !ctx.IsSet("raw") && !ctx.IsSet("url") {
123+
raw = !bytes.HasSuffix(bytes.TrimSpace(data), []byte("="))
124+
url = bytes.Contains(data, []byte("-")) || bytes.Contains(data, []byte("_"))
125+
}
126+
127+
if raw {
128+
if url {
129+
return base64.RawURLEncoding
130+
}
131+
return base64.RawStdEncoding
132+
}
133+
if url {
134+
return base64.URLEncoding
135+
}
136+
137+
return base64.StdEncoding
138+
}

command/crypto/jwt/sign.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ func readPayload(filename string) (interface{}, error) {
372372
if err != nil {
373373
return nil, errors.Wrap(err, "error reading data")
374374
}
375-
if st.Size() == 0 {
375+
if st.Size() == 0 && st.Mode()&os.ModeNamedPipe == 0 {
376376
return make(map[string]interface{}), nil
377377
}
378378
r = os.Stdin

utils/read.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import (
1818
// indicates STDIN as a file to be read.
1919
const stdinFilename = "-"
2020

21+
// stdin points to os.Stdin.
22+
var stdin = os.Stdin
23+
2124
// FileExists is a wrapper on os.Stat that returns false if os.Stat returns an
2225
// error, it returns true otherwise. This method does not care if os.Stat
2326
// returns any other kind of errors.
@@ -69,26 +72,24 @@ func ReadStringPasswordFromFile(filename string) (string, error) {
6972
// ReadInput from stdin if something is detected or ask the user for an input
7073
// using the given prompt.
7174
func ReadInput(prompt string) ([]byte, error) {
72-
st, err := os.Stdin.Stat()
75+
st, err := stdin.Stat()
7376
if err != nil {
7477
return nil, errors.Wrap(err, "error reading data")
7578
}
7679

77-
if st.Size() > 0 {
78-
return ReadAll(os.Stdin)
80+
if st.Size() == 0 && st.Mode()&os.ModeNamedPipe == 0 {
81+
return ui.PromptPassword(prompt)
7982
}
8083

81-
return ui.PromptPassword(prompt)
84+
return ReadAll(stdin)
8285
}
8386

84-
var _osStdin = os.Stdin
85-
8687
// ReadFile returns the contents of the file identified by name. It reads from
8788
// STDIN if name is a hyphen ("-").
8889
func ReadFile(name string) (b []byte, err error) {
8990
if name == stdinFilename {
9091
name = "/dev/stdin"
91-
b, err = ioutil.ReadAll(_osStdin)
92+
b, err = ioutil.ReadAll(stdin)
9293
} else {
9394
b, err = ioutil.ReadFile(name)
9495
}

utils/read_test.go

Lines changed: 122 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,42 @@ package utils
22

33
import (
44
"bytes"
5+
"fmt"
56
"io"
67
"io/ioutil"
78
"os"
9+
"reflect"
810
"testing"
911

1012
"github.com/stretchr/testify/require"
1113
)
1214

15+
type mockReader struct {
16+
n int
17+
err error
18+
}
19+
20+
func (r *mockReader) Read(p []byte) (int, error) {
21+
return r.n, r.err
22+
}
23+
1324
// Helper function for setting os.Stdin for mocking in tests.
1425
func setStdin(new *os.File) (cleanup func()) {
15-
old := _osStdin
16-
_osStdin = new
17-
return func() { _osStdin = old }
26+
old := stdin
27+
stdin = new
28+
return func() { stdin = old }
29+
}
30+
31+
// Returns a temp file and a cleanup function to delete it.
32+
func newFile(t *testing.T, data []byte) (file *os.File, cleanup func()) {
33+
f, err := ioutil.TempFile("" /* dir */, "utils-read-test")
34+
require.NoError(t, err)
35+
// write to temp file and reset read cursor to beginning of file
36+
_, err = f.Write(data)
37+
require.NoError(t, err)
38+
_, err = f.Seek(0, io.SeekStart)
39+
require.NoError(t, err)
40+
return f, func() { os.Remove(f.Name()) }
1841
}
1942

2043
func TestFileExists(t *testing.T) {
@@ -43,6 +66,66 @@ func TestFileExists(t *testing.T) {
4366
}
4467
}
4568

69+
func TestReadAll(t *testing.T) {
70+
content := []byte("read all this")
71+
72+
type args struct {
73+
r io.Reader
74+
}
75+
tests := []struct {
76+
name string
77+
args args
78+
want []byte
79+
wantErr bool
80+
}{
81+
{"ok", args{bytes.NewReader(content)}, content, false},
82+
{"fail", args{&mockReader{err: fmt.Errorf("this is an error")}}, []byte{}, true},
83+
}
84+
for _, tt := range tests {
85+
t.Run(tt.name, func(t *testing.T) {
86+
got, err := ReadAll(tt.args.r)
87+
if (err != nil) != tt.wantErr {
88+
t.Errorf("ReadAll() error = %v, wantErr %v", err, tt.wantErr)
89+
return
90+
}
91+
if !reflect.DeepEqual(got, tt.want) {
92+
t.Errorf("ReadAll() = %v, want %v", got, tt.want)
93+
}
94+
})
95+
}
96+
}
97+
98+
func TestReadString(t *testing.T) {
99+
c1 := []byte("read all this")
100+
c2 := []byte("read all this\n and all that")
101+
102+
type args struct {
103+
r io.Reader
104+
}
105+
tests := []struct {
106+
name string
107+
args args
108+
want string
109+
wantErr bool
110+
}{
111+
{"ok", args{bytes.NewReader(c1)}, "read all this", false},
112+
{"ok with new line", args{bytes.NewReader(c2)}, "read all this", false},
113+
{"fail", args{&mockReader{err: fmt.Errorf("this is an error")}}, "", true},
114+
}
115+
for _, tt := range tests {
116+
t.Run(tt.name, func(t *testing.T) {
117+
got, err := ReadString(tt.args.r)
118+
if (err != nil) != tt.wantErr {
119+
t.Errorf("ReadString() error = %v, wantErr %v", err, tt.wantErr)
120+
return
121+
}
122+
if got != tt.want {
123+
t.Errorf("ReadString() = %v, want %v", got, tt.want)
124+
}
125+
})
126+
}
127+
}
128+
46129
func TestReadFile(t *testing.T) {
47130
content := []byte("my file content")
48131
f, cleanup := newFile(t, content)
@@ -84,14 +167,40 @@ func TestStringReadPasswordFromFile(t *testing.T) {
84167
require.Equal(t, "my-password-on-file", s, "expected %s to equal %s", s, content)
85168
}
86169

87-
// Returns a temp file and a cleanup function to delete it.
88-
func newFile(t *testing.T, data []byte) (file *os.File, cleanup func()) {
89-
f, err := ioutil.TempFile("" /* dir */, "utils-read-test")
90-
require.NoError(t, err)
91-
// write to temp file and reset read cursor to beginning of file
92-
_, err = f.Write(data)
93-
require.NoError(t, err)
94-
_, err = f.Seek(0, io.SeekStart)
95-
require.NoError(t, err)
96-
return f, func() { os.Remove(f.Name()) }
170+
func TestReadInput(t *testing.T) {
171+
172+
type args struct {
173+
prompt string
174+
}
175+
tests := []struct {
176+
name string
177+
args args
178+
before func() func()
179+
want []byte
180+
wantErr bool
181+
}{
182+
{"ok", args{"Write input"}, func() func() {
183+
content := []byte("my file content")
184+
mockStdin, cleanup := newFile(t, content)
185+
reset := setStdin(mockStdin)
186+
return func() {
187+
defer cleanup()
188+
reset()
189+
}
190+
}, []byte("my file content"), false},
191+
}
192+
for _, tt := range tests {
193+
t.Run(tt.name, func(t *testing.T) {
194+
cleanup := tt.before()
195+
defer cleanup()
196+
got, err := ReadInput(tt.args.prompt)
197+
if (err != nil) != tt.wantErr {
198+
t.Errorf("ReadInput() error = %v, wantErr %v", err, tt.wantErr)
199+
return
200+
}
201+
if !reflect.DeepEqual(got, tt.want) {
202+
t.Errorf("ReadInput() = %v, want %v", got, tt.want)
203+
}
204+
})
205+
}
97206
}

0 commit comments

Comments
 (0)