diff --git a/otp/README.md b/otp/README.md new file mode 100644 index 0000000..dcb8ddd --- /dev/null +++ b/otp/README.md @@ -0,0 +1,25 @@ +# otp + +Напишите код, который реализует схему шифрования [stream cipher](https://en.wikipedia.org/wiki/Stream_cipher). + +Потоковый шифр обрабатывает поток по одному байту за раз. Каждый байт входного потока xor-ится с байтом из prng и записывается в выходной поток. + +Вам нужно реализовать две версии api. + +```golang +func NewReader(r io.Reader, prng io.Reader) io.Reader +func NewWriter(w io.Writer, prng io.Reader) io.Writer +``` + +* `NewReader` принимает входной поток `r` и генератор случайных чисел `prng`. `NewReader` возвращает + `io.Reader`, который читает поток из `r` и расшифровывает его с помощью `prng`. +* `NewWriter` принимает выходной поток `w` и генератор случайных чисел `prng`. `NewWriter` возвращает + `io.Writer`, который шифрует поток с помощью `prng` и пишет его в `w`. + +Вы можете считать, что prng никогда не может вернуть ошибку. + +## Замечания + - Прочитайте контракт [io.Reader](https://golang.org/pkg/io/#Reader) и [io.Writer](https://golang.org/pkg/io/#Writer) в документации. + - То что шифр работает с одним байтом, не значит что нужно передавать в Read() слайс размера 1. + - Подумайте, почему потоковый шифр в стандартной библиотеке имеет интерфейс [cipher.Stream](https://golang.org/pkg/crypto/cipher/#Stream), а не `io.Reader` как у нас. + - Для отладки вы можете использовать `iotest.NewReadLogger` и `iotest.NewWriteLogger` из пакета [iotest](https://golang.org/pkg/testing/iotest/). diff --git a/otp/cipher.go b/otp/cipher.go new file mode 100644 index 0000000..a9327dc --- /dev/null +++ b/otp/cipher.go @@ -0,0 +1,15 @@ +// +build !solution + +package otp + +import ( + "io" +) + +func NewReader(r io.Reader, prng io.Reader) io.Reader { + panic("implement me") +} + +func NewWriter(w io.Writer, prng io.Reader) io.Writer { + panic("implement me") +} diff --git a/otp/cipher_test.go b/otp/cipher_test.go new file mode 100644 index 0000000..2090dc6 --- /dev/null +++ b/otp/cipher_test.go @@ -0,0 +1,143 @@ +package otp + +import ( + "bytes" + "io" + "io/ioutil" + "math/rand" + "testing" + "testing/iotest" + + "github.com/stretchr/testify/require" +) + +type zeroSometimesReader struct { + io.Reader + i int +} + +func (r *zeroSometimesReader) Read(p []byte) (n int, err error) { + r.i++ + if r.i&1 == 0 { + return 0, nil + } + + return r.Reader.Read(p) +} + +const testSize = 1234 + +var ( + plaintext = make([]byte, testSize) + randomBytes = make([]byte, testSize) + ciphertext = make([]byte, testSize) + + plaintextBackup = make([]byte, testSize) +) + +func init() { + rand.Read(plaintext) + rand.Read(randomBytes) + + copy(plaintextBackup, plaintext) + + for i := range plaintext { + ciphertext[i] = plaintext[i] ^ randomBytes[i] + } +} + +func TestReader(t *testing.T) { + for _, testCase := range []struct { + name string + r io.Reader + prng io.Reader + + err error + result []byte + }{ + { + name: "simple", + r: bytes.NewBuffer(plaintext), + prng: bytes.NewBuffer(randomBytes), + result: ciphertext, + }, + { + name: "eof", + r: iotest.DataErrReader(bytes.NewBuffer(plaintext)), + prng: bytes.NewBuffer(randomBytes), + result: ciphertext, + }, + { + name: "halfreader", + r: iotest.HalfReader(bytes.NewBuffer(plaintext)), + prng: bytes.NewBuffer(randomBytes), + result: ciphertext, + }, + { + name: "zerosometimes", + r: &zeroSometimesReader{Reader: iotest.HalfReader(iotest.HalfReader(bytes.NewBuffer(plaintext)))}, + prng: bytes.NewBuffer(randomBytes), + result: ciphertext, + }, + { + name: "timeout", + r: iotest.TimeoutReader(bytes.NewBuffer(plaintext)), + prng: bytes.NewBuffer(randomBytes), + result: ciphertext[:bytes.MinRead], + err: iotest.ErrTimeout, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + r := NewReader(testCase.r, testCase.prng) + + buf, err := ioutil.ReadAll(r) + require.Equal(t, testCase.err, err) + require.Equal(t, testCase.result, buf) + }) + } +} + +func TestWriterSimple(t *testing.T) { + out := &bytes.Buffer{} + prng := bytes.NewBuffer(randomBytes) + + w := NewWriter(out, prng) + n, err := w.Write(plaintext) + + require.Equalf(t, plaintextBackup, plaintext, "Write must not modify the slice data, even temporarily.") + require.NoError(t, err) + require.Equal(t, len(plaintext), n) + require.Equal(t, out.Bytes(), ciphertext) +} + +type errWriter struct { + buf bytes.Buffer + n int +} + +func (w *errWriter) Write(p []byte) (n int, err error) { + if len(p) > w.n { + p = p[:w.n] + } + + n = len(p) + if w.n == 0 { + err = iotest.ErrTimeout + } + + w.buf.Write(p) + return +} + +func TestWriterError(t *testing.T) { + out := &errWriter{n: 512} + prng := bytes.NewBuffer(randomBytes) + + w := NewWriter(out, prng) + n, err := w.Write(plaintext) + + require.Equalf(t, plaintextBackup, plaintext, "Write must not modify the slice data, even temporarily.") + require.NoError(t, err) + require.Equal(t, 512, n) + require.Equal(t, out.buf.Bytes(), ciphertext[:512]) +}