Skip to content

Commit

Permalink
obfuscator
Browse files Browse the repository at this point in the history
  • Loading branch information
rusq committed Feb 11, 2023
1 parent 90b863e commit b179005
Show file tree
Hide file tree
Showing 6 changed files with 411 additions and 1 deletion.
1 change: 1 addition & 0 deletions cmd/slackdump/internal/diag/diag.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ open an issue on Github.
CmdRawOutput,
CmdEzTest,
CmdThread,
CmdObfuscate,
},
}
69 changes: 69 additions & 0 deletions cmd/slackdump/internal/diag/obfuscate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package diag

import (
"context"
"io"
"math/rand"
"os"
"time"

"github.com/rusq/slackdump/v2/cmd/slackdump/internal/golang/base"
"github.com/rusq/slackdump/v2/internal/processors/obfuscate"
)

// CmdObfuscate is the command to obfuscate sensitive data in a slackdump
// recording.
var CmdObfuscate = &base.Command{
UsageLine: "slackdump diag obfuscate [options] [file]",
Short: "obfuscate sensitive data in a slackdump recording",
Long: `
# Diagnostic Command: Obfuscate
`,
CustomFlags: true,
PrintFlags: true,
}

var obfuscateParams struct {
inputFile string
outputFile string
}

func init() {
rand.Seed(time.Now().UnixNano())
CmdObfuscate.Run = runObfuscate

CmdObfuscate.Flag.StringVar(&obfuscateParams.inputFile, "i", "", "input file, if not specified, stdin is used")
CmdObfuscate.Flag.StringVar(&obfuscateParams.outputFile, "o", "", "output file, if not specified, stdout is used")
}

func runObfuscate(ctx context.Context, cmd *base.Command, args []string) error {
if err := CmdObfuscate.Flag.Parse(args); err != nil {
return err
}

var (
in io.ReadCloser
out io.WriteCloser
err error
)
if obfuscateParams.inputFile == "" {
in = os.Stdin
} else {
in, err = os.Open(obfuscateParams.inputFile)
if err != nil {
return err
}
defer in.Close()
}

if obfuscateParams.outputFile == "" {
out = os.Stdout
} else {
out, err = os.Create(obfuscateParams.outputFile)
if err != nil {
return err
}
defer out.Close()
}
return obfuscate.Do(ctx, out, in)
}
246 changes: 246 additions & 0 deletions internal/processors/obfuscate/obfuscate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
// Package obfuscate obfuscates a slackdump event recording. It provides
// deterministic obfuscation of IDs, so that the users within the obfuscated
// file will have a consistent IDs. But the same file obfuscated multiple
// times will have different IDs. The text is replaced with the randomness of
// the same size + a random addition.
package obfuscate

import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"hash"
"io"
"math/rand"
"runtime/trace"
"strings"
"time"

"github.com/rusq/slackdump/v2/internal/processors"
"github.com/slack-go/slack"
)

type doOpts struct {
seed int64
}

type Option func(*doOpts)

// WithSeed allows you to specify the seed for the random number generator.
func WithSeed(seed int64) Option {
return func(opts *doOpts) {
opts.seed = seed
}
}

func Do(ctx context.Context, w io.Writer, r io.Reader, options ...Option) error {
_, task := trace.NewTask(ctx, "obfuscate.Do")
defer task.End()

var opts = doOpts{
seed: time.Now().UnixNano(),
}
for _, optFn := range options {
optFn(&opts)
}
rand.Seed(opts.seed)

var (
dec = json.NewDecoder(r)
enc = json.NewEncoder(w)
obf = obfuscator{
hasher: sha256.New,
salt: randomStringExact(32),
}
)
// obfuscation loop
for {
var e processors.Event
if err := dec.Decode(&e); err != nil {
if err == io.EOF {
break
}
return err
}
trace.WithRegion(ctx, "obfuscate.Event", func() {
obf.Event(&e)
})
if err := enc.Encode(e); err != nil {
return err
}
}
return nil
}

type obfuscator struct {
hasher func() hash.Hash
salt string
}

func (o obfuscator) Event(e *processors.Event) {
e.ChannelID = o.ID("C", e.ChannelID)
switch e.Type {
case processors.EventMessages:
o.Messages(e.Messages...)
case processors.EventThreadMessages:
o.OneMessage(e.Parent)
o.Messages(e.Messages...)
case processors.EventFiles:
o.OneMessage(e.Parent)
o.Files(e.Files...)
}
}

// obfuscateManyMessages obfuscates a slice of messages.
func (o obfuscator) Messages(m ...slack.Message) {
for i := range m {
o.OneMessage(&m[i])
}
}

const filePrefix = "https://files.slack.com/"

func (o obfuscator) OneMessage(m *slack.Message) {
if m == nil {
return
}
m.ClientMsgID = randomUUID()
m.Team = o.ID("T", m.Team)
m.User = o.ID("U", m.User)
if m.Text != "" {
m.Text = randomString(len(m.Text))
}
if m.Edited != nil {
m.Edited.User = o.ID("U", m.Edited.User)
}
if len(m.Blocks.BlockSet) > 0 {
m.Blocks.BlockSet = nil // too much hassle to obfuscate
}
if len(m.Reactions) > 0 {
o.Reactions(m.Reactions)
}
if len(m.Attachments) > 0 {
m.Attachments = nil // too much hassle to obfuscate
}
for i := range m.Files {
o.OneFile(&m.Files[i])
}
}

func (o obfuscator) Files(f ...slack.File) {
for i := range f {
o.OneFile(&f[i])
}
}

func (o obfuscator) OneFile(f *slack.File) {
if f == nil {
return
}
ifnotnil := func(s string) string {
if s != "" {
if strings.HasPrefix(s, filePrefix) {
s = filePrefix + randomString(len(s)-len(filePrefix))
} else {
s = randomString(len(s))
}
}
return s
}
fields := []*string{
&f.URLPrivate,
&f.URLPrivateDownload,
&f.Permalink,
&f.PermalinkPublic,
&f.Thumb64,
&f.Thumb80,
&f.Thumb360,
&f.Thumb360Gif,
&f.Thumb480,
&f.Thumb160,
&f.Thumb720,
&f.Thumb960,
&f.Thumb1024,
}
for i := range fields {
*fields[i] = ifnotnil(*fields[i])
}
f.Title = randomString(len(f.Title))
f.Name = randomString(len(f.Name))
f.Thumb360W = 0
f.Thumb360H = 0
f.Thumb480W = 0
f.Thumb480H = 0
f.Thumb720W = 0
f.Thumb720H = 0
f.Thumb960W = 0
f.Thumb960H = 0
f.Thumb1024W = 0
f.Thumb1024H = 0
f.OriginalW = 0
f.OriginalH = 0
f.InitialComment = slack.Comment{}
f.User = o.ID("U", f.User)
f.ID = o.ID("F", f.ID)
}

// randomString returns a random string of length n + random number [0,40).
func randomString(n int) string {
return rndstr(n, rand.Intn(40))
}

// randomStringExact returns a random string of length n.
func randomStringExact(n int) string {
return rndstr(n, 0)
}

// rndstr returns a random string of length base+add.
func rndstr(base int, add int) string {
var (
b = make([]byte, base+add)
src = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ")
)
for i := range b {
b[i] = src[rand.Intn(len(src))]
}
return string(b)
}

// randomUUID returns a random UUID.
func randomUUID() string {
var (
b = make([]byte, 36)
src = []byte("0123456789abcdef")
)
for i := range b {
switch i {
case 8, 13, 18, 23:
b[i] = '-'
default:
b[i] = src[rand.Intn(len(src))]
}
}
return string(b)
}

// ID obfuscates an ID.
func (o obfuscator) ID(prefix string, id string) string {
if id == "" {
return ""
}
h := o.hasher()
if _, err := h.Write([]byte(o.salt + id)); err != nil {
panic(err)
}
return prefix + strings.ToUpper(hex.EncodeToString(h.Sum(nil)))[:len(id)-1]
}

func (o obfuscator) Reactions(r []slack.ItemReaction) {
for i := range r {
r[i].Name = randomStringExact(len(r[i].Name))
for j := range r[i].Users {
r[i].Users[j] = o.ID("U", r[i].Users[j])
}
}
}
44 changes: 44 additions & 0 deletions internal/processors/obfuscate/obfuscate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package obfuscate

import (
"math/rand"
"testing"
)

func init() {
rand.Seed(0) // make it deterministic
}

func Test_randomString(t *testing.T) {
type args struct {
n int
}
tests := []struct {
name string
args args
want string
}{
{
name: "empty",
args: args{n: 0},
want: "jXUJR9JT5pul5g8MDbK7E1ycTwBhzdJG9 ",
},
{
name: "one",
args: args{n: 1},
want: "VwGabEN7FkWNmyD0HtOdvcYYvfHfF hVA6",
},
{
name: "100",
args: args{n: 100},
want: "d1BtVOw52BH40tQ4xsZr1rbOEdndtLrooKH5L9GzLgWmmWfVTBKfSvym98qEQMYaWdLEKrJCEXzYB2bFiOLzhKfgf0hdxneHm6GIP4BlU7M3cWoFQL4mevBBbRf",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := randomString(tt.args.n); got != tt.want {
t.Errorf("randomString() = %v, want %v", got, tt.want)
}
})
}
}
Loading

0 comments on commit b179005

Please sign in to comment.