[ISSUE-67869881] add cipher
diff --git a/.travis.yml b/.travis.yml
index 5080b8b..64f6935 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -14,4 +14,4 @@
 script:
   - diff -u <(echo -n) <(gofmt -d $(git ls-files | grep '.go$' | grep -v vendor))
   - go vet $(glide novendor)
-  - go test $(glide novendor)
+  - go test -covermode=atomic $(glide novendor)
diff --git a/cipher/cipher.go b/cipher/cipher.go
new file mode 100644
index 0000000..06d8242
--- /dev/null
+++ b/cipher/cipher.go
@@ -0,0 +1,186 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package cipher
+
+import (
+	"crypto/aes"
+	"crypto/cipher"
+	"fmt"
+	"sync"
+)
+
+type Mode string
+type Padding string
+
+const (
+	ModeEcb = Mode("ECB")
+)
+
+const (
+	PaddingPKCS7 = Padding("PKCS7Padding")
+	PaddingPKCS5 = Padding("PKCS5Padding")
+)
+
+var (
+	supportedModes = map[Mode]bool{
+		ModeEcb: true,
+	}
+	supportedPadding = map[Padding]bool{
+		PaddingPKCS7: true,
+		PaddingPKCS5: true,
+	}
+)
+
+func (p Padding) padding(input []byte) (output []byte) {
+	switch p {
+	case PaddingPKCS5:
+		fallthrough
+	case PaddingPKCS7:
+		//PKCS7Padding
+		numPad := 16 - (len(input) % 16)
+		output = make([]byte, len(input)+numPad)
+		for i := copy(output, []byte(input)); i < len(output); i++ {
+			output[i] = byte(numPad)
+		}
+	}
+	return
+}
+
+func (p Padding) strip(input []byte) (output []byte) {
+	switch p {
+	case PaddingPKCS5:
+		fallthrough
+	case PaddingPKCS7:
+		//remove PKCS7Padding
+		numPad := int(input[len(input)-1])
+		output = input[:(len(input) - numPad)]
+	}
+	return
+}
+
+// Create a new AesCipher object with the specified encryption mode and padding algorithm.
+func CreateAesCipher(key []byte) (*AesCipher, error) {
+	a := &AesCipher{
+		mutex: &sync.RWMutex{},
+	}
+	if err := a.SetKey(key); err != nil {
+		return nil, err
+	}
+	return a, nil
+}
+
+// An object to perform AES encryption/decryption.
+type AesCipher struct {
+	key   []byte
+	block cipher.Block
+	mutex *sync.RWMutex
+}
+
+// Set/Change the AES key, accepted key's bit-size is 128/192/256.
+func (a *AesCipher) SetKey(key []byte) (err error) {
+	a.mutex.Lock()
+	defer a.mutex.Unlock()
+	a.key = key
+	a.block, err = aes.NewCipher(key)
+	return err
+}
+
+// Encrypt the plaintext. Padding is performed before encryption, so the
+// plaintext input can have any length.
+func (a *AesCipher) Encrypt(plaintext []byte, mode Mode, padding Padding) (ciphertext []byte, err error) {
+	// check mode
+	if !supportedModes[mode] {
+		return nil, &ErrModeUnsupported{
+			mode: mode,
+		}
+	}
+	// check padding
+	if !supportedPadding[padding] {
+		return nil, &ErrPaddingUnsupported{
+			padding: padding,
+		}
+	}
+	// padding
+	text := padding.padding(plaintext)
+	ciphertext = text
+
+	// encrypt
+	a.mutex.RLock()
+	block := a.block
+	a.mutex.RUnlock()
+
+	switch mode {
+	case ModeEcb:
+		size := block.BlockSize()
+		for len(text) > 0 {
+			block.Encrypt(text, text)
+			text = text[size:]
+		}
+	}
+	return
+}
+
+// Decrypt the ciphertext. Padding is removed after decryption, so the
+// plaintext output can have different length from input ciphertext.
+func (a *AesCipher) Decrypt(ciphertext []byte, mode Mode, padding Padding) (plaintext []byte, err error) {
+	// check mode
+	if !supportedModes[mode] {
+		return nil, &ErrModeUnsupported{
+			mode: mode,
+		}
+	}
+	// check padding
+	if !supportedPadding[padding] {
+		return nil, &ErrPaddingUnsupported{
+			padding: padding,
+		}
+	}
+
+	// encrypt
+	a.mutex.RLock()
+	block := a.block
+	a.mutex.RUnlock()
+
+	switch mode {
+	case ModeEcb:
+		plaintext = make([]byte, len(ciphertext))
+		buffer := plaintext
+		size := block.BlockSize()
+		for len(ciphertext) > 0 {
+			block.Decrypt(buffer, ciphertext)
+			ciphertext = ciphertext[size:]
+			buffer = buffer[size:]
+		}
+	}
+
+	// strip padding
+	plaintext = padding.strip(plaintext)
+	return
+}
+
+type ErrModeUnsupported struct {
+	mode Mode
+}
+
+func (e *ErrModeUnsupported) Error() string {
+	return fmt.Sprintf("mode unsupported: %v", e.mode)
+}
+
+type ErrPaddingUnsupported struct {
+	padding Padding
+}
+
+func (e *ErrPaddingUnsupported) Error() string {
+	return fmt.Sprintf("padding unsupported: %v", e.padding)
+}
diff --git a/cipher/cipher_test.go b/cipher/cipher_test.go
new file mode 100644
index 0000000..0bdde75
--- /dev/null
+++ b/cipher/cipher_test.go
@@ -0,0 +1,118 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cipher_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	"github.com/apid/apid-core/cipher"
+	"testing"
+)
+
+func TestEvents(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Cipher Suite")
+}
+
+var _ = Describe("APID Cipher", func() {
+
+	Context("AES", func() {
+
+		Context("AES/ECB/PKCS7Padding Encrypt/Decrypt", func() {
+			type testData struct {
+				key        []byte
+				plaintext  []byte
+				ciphertext []byte
+			}
+
+			data := []testData{
+				{
+					// 128-bit
+					[]byte{2, 122, 212, 83, 150, 164, 180, 4, 148, 242, 65, 189, 3, 188, 76, 247},
+					[]byte("aUWQKgAwmaR0p2kY"),
+					// 32-byte after padding
+					[]byte{218, 53, 247, 87, 119, 80, 231, 16, 125, 11, 214, 101, 246, 202, 178, 163, 202, 102,
+						146, 245, 79, 215, 74, 228, 17, 83, 213, 134, 105, 203, 31, 14},
+				},
+				{
+					// 192-bit
+					[]byte{2, 122, 212, 83, 150, 164, 180, 4, 148, 242, 65, 189, 3, 188, 76, 247,
+						2, 122, 212, 83, 150, 164, 180, 4},
+					[]byte("a"),
+					// 16-byte after padding
+					[]byte{225, 2, 177, 65, 152, 88, 116, 43, 71, 215, 84, 240, 221, 175, 11, 131},
+				},
+				{
+					// 256-bit
+					[]byte{2, 122, 212, 83, 150, 164, 180, 4, 148, 242, 65, 189, 3, 188, 76, 247,
+						2, 122, 212, 83, 150, 164, 180, 4, 148, 242, 65, 189, 3, 188, 76, 247},
+					[]byte(""),
+					// 16-byte after padding
+					[]byte{88, 192, 164, 235, 153, 89, 14, 134, 224, 122, 31, 36, 238, 117, 121, 117},
+				},
+			}
+			It("Encrypt", func() {
+				for i := 0; i < len(data); i++ {
+					c, err := cipher.CreateAesCipher(data[i].key)
+					Expect(err).Should(Succeed())
+					Expect(c.Encrypt(data[i].plaintext, cipher.ModeEcb, cipher.PaddingPKCS5)).Should(Equal(data[i].ciphertext))
+					Expect(c.Encrypt(data[i].plaintext, cipher.ModeEcb, cipher.PaddingPKCS7)).Should(Equal(data[i].ciphertext))
+				}
+			})
+
+			It("Decrypt", func() {
+				for i := 0; i < len(data); i++ {
+					c, err := cipher.CreateAesCipher(data[i].key)
+					Expect(err).Should(Succeed())
+					Expect(c.Encrypt(data[i].plaintext, cipher.ModeEcb, cipher.PaddingPKCS5)).Should(Equal(data[i].ciphertext))
+					Expect(c.Encrypt(data[i].plaintext, cipher.ModeEcb, cipher.PaddingPKCS7)).Should(Equal(data[i].ciphertext))
+				}
+			})
+		})
+
+		It("SetKey", func() {
+			key := make([]byte, 16)
+			plaintext := []byte("aUWQKgAwmaR0p2kY")
+			ciphertext := []byte{218, 53, 247, 87, 119, 80, 231, 16, 125, 11, 214, 101, 246, 202, 178, 163, 202, 102, 146, 245, 79, 215, 74, 228, 17, 83, 213, 134, 105, 203, 31, 14}
+			c, err := cipher.CreateAesCipher(key)
+			Expect(err).Should(Succeed())
+			key = []byte{2, 122, 212, 83, 150, 164, 180, 4, 148, 242, 65, 189, 3, 188, 76, 247}
+			Expect(c.SetKey(key)).Should(Succeed())
+			Expect(c.Encrypt(plaintext, cipher.ModeEcb, cipher.PaddingPKCS5)).Should(Equal(ciphertext))
+			Expect(c.Decrypt(ciphertext, cipher.ModeEcb, cipher.PaddingPKCS7)).Should(Equal(plaintext))
+		})
+
+		It("Invalid Parameters", func() {
+			_, err := cipher.CreateAesCipher(make([]byte, 15))
+			Expect(err).ToNot(Succeed())
+			_, err = cipher.CreateAesCipher(nil)
+			Expect(err).ToNot(Succeed())
+			key := make([]byte, 16)
+			c, err := cipher.CreateAesCipher(key)
+			Expect(err).Should(Succeed())
+			_, err = c.Encrypt([]byte{1, 2, 3}, cipher.Mode("unsupported"), cipher.PaddingPKCS7)
+			Expect(err).ToNot(Succeed())
+			_, err = c.Encrypt([]byte{1, 2, 3}, cipher.ModeEcb, cipher.Padding("unsupported"))
+			Expect(err).ToNot(Succeed())
+			_, err = c.Decrypt([]byte{88, 192, 164, 235, 153, 89, 14, 134, 224, 122, 31, 36, 238, 117, 121, 117},
+				cipher.Mode("unsupported"), cipher.PaddingPKCS7)
+			Expect(err).ToNot(Succeed())
+			_, err = c.Decrypt([]byte{88, 192, 164, 235, 153, 89, 14, 134, 224, 122, 31, 36, 238, 117, 121, 117},
+				cipher.ModeEcb, cipher.Padding("unsupported"))
+			Expect(err).ToNot(Succeed())
+		})
+	})
+})