Last active
May 22, 2019 08:27
-
-
Save auxten/451eebc568007de9552f0431ca6a80a6 to your computer and use it in GitHub Desktop.
AES-CBC-PKCS#5 (PKCS#7) with KDF and salt implementation and tests in Java, Golang, Python, JavaScript from github.com/CovenantSQL
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
var test = require('ava'), | |
aes = require('aes-js'), | |
e2e = require('..'); | |
function from_hex(s) { | |
return new Uint8Array(aes.utils.hex.toBytes(s)); | |
} | |
test('test golang decryption cases', function(t) { | |
// following tests are migrated from end-to-end encryption golang implementation | |
const cases = [ | |
{ | |
raw: '11', | |
pass: ';#K]As9C*6L', | |
possibleEncrypted: 'a372ea2c158a2f99d386e309db4355a659a7a8dd3986fd1d94f7604256061609', | |
}, | |
{ | |
raw: '111282C128421286712857128C2128EF' + | |
'128B7671283C128571287512830128EC' + | |
'128391281A1312849128381281E1286A' + | |
'12871128621287A9D12857128C412886' + | |
'128FD12834128DA128F5', | |
pass: '', | |
possibleEncrypted: '1bfb6a7fda3e3eb1e14c9afd0baefe86' + | |
'c90979101f179db7e48a0fa7617881e8' + | |
'f752c59fb512bb86b8ed69c5644bf2dc' + | |
'30fbcd3bf79fb20342595c84fad00e46' + | |
'2fab3e51266492a3d5d085e650c1e619' + | |
'6278d7f5185c263440ec6fd940ffbb85', | |
}, | |
{ | |
raw: '11', | |
pass: '\'K]"#\'pi/1/JD2', | |
possibleEncrypted: 'a83d152777ce3a1c0710b03676ae867c86ab0a47b3ca080f825683ac1079eb41', | |
}, | |
{ | |
raw: '11111111111111111111111111111111', | |
pass: '', | |
possibleEncrypted: '7dda438c4256a63c62d6816617fcbf9c' + | |
'7773b9b4f87902b7253848ba2b0ed0ba' + | |
'f70a3ac976a835b7bc3008e9ba43da74', | |
}, | |
{ | |
raw: '11111111111111111111111111111111', | |
pass: 'youofdas1312', | |
possibleEncrypted: 'cab07967cf377dbc010fbf5f84d12bcb' + | |
'6f8b188e6965738cf9007a671b4bfeb9' + | |
'f52257aac3808048c341dcaa1c125ca7', | |
}, | |
{ | |
raw: '11111111111111111111111111', | |
pass: '空のBottle😄', | |
possibleEncrypted: '4384874473945c5b70519ad5ace6305ef6b78c60c3c694add08a8b81899c4171', | |
}, | |
]; | |
cases.forEach(function(c) { | |
const d = e2e.decrypt(from_hex(c.possibleEncrypted), c.pass); | |
t.deepEqual(d, from_hex(c.raw)); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2019 The CovenantSQL Authors. | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except raw 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 raw 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 toolkit | |
import ( | |
"crypto/aes" | |
"crypto/cipher" | |
"crypto/rand" | |
"errors" | |
"io" | |
"github.com/CovenantSQL/CovenantSQL/crypto" | |
"github.com/CovenantSQL/CovenantSQL/crypto/symmetric" | |
) | |
var salt = [...]byte{ | |
0x3f, 0xb8, 0x87, 0x7d, 0x37, 0xfd, 0xc0, 0x4e, | |
0x4a, 0x47, 0x65, 0xEF, 0xb8, 0xab, 0x7d, 0x36, | |
} | |
// Encrypt encrypts data with given password by AES-128-CBC PKCS#7, iv will be placed | |
// at head of cipher data. | |
func Encrypt(in, password []byte) (out []byte, err error) { | |
// keyE will be 128 bits, so aes.NewCipher(keyE) will return | |
// AES-128 Cipher. | |
keyE := symmetric.KeyDerivation(password, salt[:])[:16] | |
paddedIn := crypto.AddPKCSPadding(in) | |
// IV + padded cipher data | |
out = make([]byte, aes.BlockSize+len(paddedIn)) | |
// as IV length must equal block size, iv length should be 128 bits | |
iv := out[:aes.BlockSize] | |
if _, err = io.ReadFull(rand.Reader, iv); err != nil { | |
return nil, err | |
} | |
// start encryption, as keyE and iv are generated properly, there should | |
// not be any error | |
block, _ := aes.NewCipher(keyE) | |
mode := cipher.NewCBCEncrypter(block, iv) | |
mode.CryptBlocks(out[aes.BlockSize:], paddedIn) | |
return out, nil | |
} | |
// Decrypt decrypts data with given password by AES-128-CBC PKCS#7. iv will be read from | |
// the head of raw. | |
func Decrypt(in, password []byte) (out []byte, err error) { | |
keyE := symmetric.KeyDerivation(password, salt[:])[:16] | |
// IV + padded cipher data == (n + 1 + 1) * aes.BlockSize | |
if len(in)%aes.BlockSize != 0 || len(in)/aes.BlockSize < 2 { | |
return nil, errors.New("cipher data size not match") | |
} | |
// read IV | |
iv := in[:aes.BlockSize] | |
// start decryption, as keyE and iv are generated properly, there should | |
// not be any error | |
block, _ := aes.NewCipher(keyE) | |
mode := cipher.NewCBCDecrypter(block, iv) | |
// same length as cipher data | |
plainData := make([]byte, len(in)-aes.BlockSize) | |
mode.CryptBlocks(plainData, in[aes.BlockSize:]) | |
return crypto.RemovePKCSPadding(plainData) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* global exports define */ | |
(function(root){ | |
'use strict'; | |
const BLOCK_SIZE = 16; | |
var pkcs7 = require('pkcs7'), | |
aes = require('aes-js'), | |
hash = require('hash.js'), | |
salt = new Uint8Array([ | |
0x3f, 0xb8, 0x87, 0x7d, 0x37, 0xfd, 0xc0, 0x4e, | |
0x4a, 0x47, 0x65, 0xEF, 0xb8, 0xab, 0x7d, 0x36, | |
]); | |
function str_to_buf(s) { | |
if (typeof(s) == 'string') { | |
const len = s.length; | |
const buf = new Uint8Array(len); | |
for (let i = 0; i != len; ++i) { | |
buf[i] = s.charCodeAt(i); | |
} | |
return buf; | |
} else { | |
return s; | |
} | |
} | |
function key_derivation(passwd) { | |
const h = hash.sha256().update(hash.sha256().update(passwd).update(salt).digest()).digest(); | |
return new Uint8Array(h.slice(0, BLOCK_SIZE)); | |
} | |
function rand_bytes(sz) { | |
const iv = new Uint8Array(sz); | |
for (let i = 0; i != sz; ++i) { | |
iv[i] = Math.floor(Math.random() * 256); | |
} | |
return iv; | |
} | |
function pad(s) { | |
return pkcs7.pad(str_to_buf(s)); | |
} | |
function unpad(s) { | |
if (!s || s.length < BLOCK_SIZE) { | |
throw new Error('invalid pkcs#7 data'); | |
} | |
const p = s[s.length - 1]; | |
if (p > BLOCK_SIZE) { | |
throw new Error('invalid pkcs#7 padding length'); | |
} | |
return pkcs7.unpad(s); | |
} | |
function encrypt(s, passwd) { | |
const p = key_derivation(passwd); | |
const iv = rand_bytes(BLOCK_SIZE); | |
const cbc = new aes.ModeOfOperation.cbc(p, iv); | |
const pad_in = pad(s); | |
const ret = new Uint8Array(iv.length + pad_in.length); | |
const ed = cbc.encrypt(pad_in); | |
if (!ed || !ed.length) { | |
throw new Error('encrypt failed'); | |
} | |
ret.set(iv); | |
ret.set(ed, iv.length); | |
return ret; | |
} | |
function decrypt(c, passwd) { | |
const p = key_derivation(passwd); | |
const cb = str_to_buf(c); | |
if (cb.length < 2 * BLOCK_SIZE) { | |
throw new Error('invalid cipher'); | |
} | |
const iv = cb.slice(0, BLOCK_SIZE); | |
const cbc = new aes.ModeOfOperation.cbc(p, iv); | |
const dd = cbc.decrypt(cb.slice(BLOCK_SIZE)); | |
if (!dd || !dd.length || dd.length != (cb.length - BLOCK_SIZE)) { | |
throw new Error('invalid decrypted data'); | |
} | |
return unpad(new Uint8Array(dd)); | |
} | |
function decrypt_string(c, passwd) { | |
const d = decrypt(c, passwd); | |
return aes.utils.utf8.fromBytes(d); | |
} | |
var e2e = { | |
encrypt: encrypt, | |
decrypt: decrypt, | |
decrypt_string: decrypt_string, | |
}; | |
if (typeof(exports) !== 'undefined') { | |
module.exports = e2e; | |
} else if (typeof(define) === 'function' && define.amd) { | |
define([], function() { | |
return e2e; | |
}); | |
} else { | |
if (root.e2e) { | |
e2e._e2e = root.e2e; | |
} | |
root.e2e = e2e; | |
} | |
})(this); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from Crypto.Cipher import AES | |
from Crypto import Random | |
import hashlib | |
from binascii import hexlify, unhexlify | |
BLOCK_SIZE = AES.block_size # Bytes | |
salt = unhexlify("3fb8877d37fdc04e4a4765EFb8ab7d36") | |
class PaddingError(Exception): | |
"""Exception raised for errors in the padding. | |
Attributes: | |
message -- explanation of the error | |
""" | |
def __init__(self, message): | |
self.message = message | |
pad = lambda s: s + ((BLOCK_SIZE - len(s) % BLOCK_SIZE) * | |
chr(BLOCK_SIZE - len(s) % BLOCK_SIZE)).encode('ascii') | |
def unpad(s): | |
in_len = len(s) | |
if in_len == 0: | |
raise PaddingError("empty input") | |
pad_char = s[-1] | |
if pad_char > BLOCK_SIZE: | |
raise PaddingError("padding length > 16") | |
for i in s[in_len - pad_char:]: | |
if i != pad_char: | |
raise PaddingError("unexpected padding char") | |
return s[:-pad_char] | |
# kdf does 2 times sha256 and takes the first 16 bytes | |
def kdf(raw_key): | |
""" | |
kdf does 2 times sha256 and takes the first 16 bytes | |
:param raw_key: | |
:return: | |
""" | |
return hashlib.sha256(hashlib.sha256(raw_key + salt).digest()).digest()[:16] | |
def encrypt(raw, password): | |
""" | |
encrypt encrypts data with given password by AES-128-CBC PKCS#7, iv will be placed | |
at head of cipher data. | |
:param raw: input raw byte array | |
:param password: password byte array | |
:return: encrypted byte array | |
""" | |
iv = Random.new().read(AES.block_size) | |
cipher = AES.new(kdf(password), AES.MODE_CBC, iv) | |
return iv + cipher.encrypt(pad(raw)) | |
def decrypt(enc, password): | |
""" | |
decrypt decrypts data with given password by AES-128-CBC PKCS#7. iv will be read from | |
the head of raw. | |
:param enc: input encrypted byte array | |
:param password: password byte array | |
:return: decrypted byte array | |
""" | |
iv = enc[:16] | |
cipher = AES.new(kdf(password), AES.MODE_CBC, iv) | |
return unpad(cipher.decrypt(enc[16:])) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2019 The CovenantSQL Authors. | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except raw 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 raw 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 toolkit | |
import ( | |
"bytes" | |
"crypto/aes" | |
"encoding/hex" | |
"testing" | |
. "github.com/smartystreets/goconvey/convey" | |
"github.com/CovenantSQL/CovenantSQL/utils/log" | |
) | |
// Test cases for all implementations. | |
// Because iv is random, so Encrypted data is not always the same, | |
// but Decrypt(possibleEncrypted) will get raw. | |
// `raw` and `possibleEncrypted` are in hex | |
// `pass` is raw string | |
var testCases = []struct { | |
raw string | |
pass string | |
possibleEncrypted string | |
}{ | |
{ | |
raw: "11", | |
pass: ";#K]As9C*6L", | |
possibleEncrypted: "a372ea2c158a2f99d386e309db4355a659a7a8dd3986fd1d94f7604256061609", | |
}, | |
{ | |
raw: "111282C128421286712857128C2128EF" + | |
"128B7671283C128571287512830128EC" + | |
"128391281A1312849128381281E1286A" + | |
"12871128621287A9D12857128C412886" + | |
"128FD12834128DA128F5", | |
pass: "", | |
possibleEncrypted: "1bfb6a7fda3e3eb1e14c9afd0baefe86" + | |
"c90979101f179db7e48a0fa7617881e8" + | |
"f752c59fb512bb86b8ed69c5644bf2dc" + | |
"30fbcd3bf79fb20342595c84fad00e46" + | |
"2fab3e51266492a3d5d085e650c1e619" + | |
"6278d7f5185c263440ec6fd940ffbb85", | |
}, | |
{ | |
raw: "11", | |
pass: "'K]\"#'pi/1/JD2", | |
possibleEncrypted: "a83d152777ce3a1c0710b03676ae867c86ab0a47b3ca080f825683ac1079eb41", | |
}, | |
{ | |
raw: "11111111111111111111111111111111", | |
pass: "", | |
possibleEncrypted: "7dda438c4256a63c62d6816617fcbf9c" + | |
"7773b9b4f87902b7253848ba2b0ed0ba" + | |
"f70a3ac976a835b7bc3008e9ba43da74", | |
}, | |
{ | |
raw: "11111111111111111111111111111111", | |
pass: "youofdas1312", | |
possibleEncrypted: "cab07967cf377dbc010fbf5f84d12bcb" + | |
"6f8b188e6965738cf9007a671b4bfeb9" + | |
"f52257aac3808048c341dcaa1c125ca7", | |
}, | |
{ | |
raw: "11111111111111111111111111", | |
pass: "空のBottle😄", | |
possibleEncrypted: "4384874473945c5b70519ad5ace6305ef6b78c60c3c694add08a8b81899c4171", | |
}, | |
} | |
func TestEncryptDecryptCases(t *testing.T) { | |
defaultLevel := log.GetLevel() | |
log.SetLevel(log.DebugLevel) | |
defer log.SetLevel(defaultLevel) | |
Convey("encrypt & decrypt cases", t, func() { | |
for i, c := range testCases { | |
in, _ := hex.DecodeString(c.raw) | |
pass := []byte(c.pass) | |
out, _ := hex.DecodeString(c.possibleEncrypted) | |
log.Infof("TestEncryptDecryptCases: %d", i) | |
enc, err := Encrypt(in, pass) | |
log.Debugf("Enc: %x", enc) | |
So(err, ShouldBeNil) | |
dec1, err := Decrypt(enc, pass) | |
if !bytes.Equal(dec1, in) { | |
t.Errorf("\nExpected:\n%x\nActual:\n%x\n", in, dec1) | |
} | |
dec2, err := Decrypt(out, pass) | |
So(err, ShouldBeNil) | |
if !bytes.Equal(dec2, in) { | |
t.Errorf("\nExpected:\n%x\nActual:\n%x\n", in, dec2) | |
} | |
} | |
}) | |
} | |
func TestEncryptDecrypt(t *testing.T) { | |
var password = "CovenantSQL.io" | |
Convey("encrypt & decrypt 0 length string with aes128", t, func() { | |
enc, err := Encrypt([]byte(""), []byte(password)) | |
So(enc, ShouldNotBeNil) | |
So(len(enc), ShouldEqual, 2*aes.BlockSize) | |
So(err, ShouldBeNil) | |
dec, err := Decrypt(enc, []byte(password)) | |
So(dec, ShouldNotBeNil) | |
So(len(dec), ShouldEqual, 0) | |
So(err, ShouldBeNil) | |
}) | |
Convey("encrypt & decrypt 0 length bytes with aes128", t, func() { | |
enc, err := Encrypt([]byte(nil), []byte(password)) | |
So(enc, ShouldNotBeNil) | |
So(len(enc), ShouldEqual, 2*aes.BlockSize) | |
So(err, ShouldBeNil) | |
dec, err := Decrypt(enc, []byte(password)) | |
So(dec, ShouldNotBeNil) | |
So(len(dec), ShouldEqual, 0) | |
So(err, ShouldBeNil) | |
}) | |
Convey("encrypt & decrypt 1 byte with aes128", t, func() { | |
enc, err := Encrypt([]byte{0x11}, []byte(password)) | |
So(enc, ShouldNotBeNil) | |
So(len(enc), ShouldEqual, 2*aes.BlockSize) | |
So(err, ShouldBeNil) | |
dec, err := Decrypt(enc, []byte(password)) | |
So(dec, ShouldResemble, []byte{0x11}) | |
So(len(dec), ShouldEqual, 1) | |
So(err, ShouldBeNil) | |
}) | |
Convey("encrypt & decrypt 1747 length bytes", t, func() { | |
in := bytes.Repeat([]byte{0xff}, 1747) | |
enc, err := Encrypt(in, []byte(password)) | |
So(enc, ShouldNotBeNil) | |
So(len(enc), ShouldEqual, (1747/aes.BlockSize+2)*aes.BlockSize) | |
So(err, ShouldBeNil) | |
dec, err := Decrypt(enc, []byte(password)) | |
So(dec, ShouldResemble, in) | |
So(len(dec), ShouldEqual, 1747) | |
So(err, ShouldBeNil) | |
}) | |
Convey("encrypt & decrypt 32 length bytes", t, func() { | |
in := bytes.Repeat([]byte{0xcc}, 32) | |
enc, err := Encrypt(in, []byte(password)) | |
So(enc, ShouldNotBeNil) | |
So(len(enc), ShouldEqual, (32/aes.BlockSize+2)*aes.BlockSize) | |
So(err, ShouldBeNil) | |
dec, err := Decrypt(enc, []byte(password)) | |
So(dec, ShouldResemble, in) | |
So(len(dec), ShouldEqual, 32) | |
So(err, ShouldBeNil) | |
}) | |
Convey("decrypt error length bytes", t, func() { | |
in := bytes.Repeat([]byte{0xaa}, 1747) | |
dec, err := Decrypt(in, []byte(password)) | |
So(dec, ShouldBeNil) | |
So(err.Error(), ShouldEqual, "cipher data size not match") | |
}) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2019 The CovenantSQL Authors. | |
* | |
* 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 io.covenantsql.connector; | |
import io.covenantsql.connector.util.EndToEndEncryption; | |
import org.apache.commons.codec.binary.Hex; | |
import org.testng.annotations.BeforeMethod; | |
import org.testng.annotations.Test; | |
import static org.testng.Assert.assertEquals; | |
class testCase { | |
String raw; | |
String password; | |
String possibleEncrypted; | |
public testCase(String raw, String password, String possibleEncrypted) { | |
this.raw = raw; | |
this.password = password; | |
this.possibleEncrypted = possibleEncrypted; | |
} | |
} | |
public class EndToEndEncryptionTests { | |
testCase[] cases; | |
@BeforeMethod | |
public void setUp() { | |
cases = new testCase[]{ | |
new testCase( | |
"11", | |
";#K]As9C*6L", | |
"a372ea2c158a2f99d386e309db4355a659a7a8dd3986fd1d94f7604256061609" | |
), | |
new testCase( | |
"111282C128421286712857128C2128EF" + | |
"128B7671283C128571287512830128EC" + | |
"128391281A1312849128381281E1286A" + | |
"12871128621287A9D12857128C412886" + | |
"128FD12834128DA128F5", | |
"", | |
"1bfb6a7fda3e3eb1e14c9afd0baefe86" + | |
"c90979101f179db7e48a0fa7617881e8" + | |
"f752c59fb512bb86b8ed69c5644bf2dc" + | |
"30fbcd3bf79fb20342595c84fad00e46" + | |
"2fab3e51266492a3d5d085e650c1e619" + | |
"6278d7f5185c263440ec6fd940ffbb85" | |
), | |
new testCase( | |
"11", | |
"'K]\"#'pi/1/JD2", | |
"a83d152777ce3a1c0710b03676ae867c86ab0a47b3ca080f825683ac1079eb41" | |
), | |
new testCase( | |
"11111111111111111111111111111111", | |
"", | |
"7dda438c4256a63c62d6816617fcbf9c" + | |
"7773b9b4f87902b7253848ba2b0ed0ba" + | |
"f70a3ac976a835b7bc3008e9ba43da74" | |
), | |
new testCase( | |
"11111111111111111111111111111111", | |
"youofdas1312", | |
"cab07967cf377dbc010fbf5f84d12bcb" + | |
"6f8b188e6965738cf9007a671b4bfeb9" + | |
"f52257aac3808048c341dcaa1c125ca7" | |
), | |
new testCase( | |
"11111111111111111111111111", | |
"空のBottle😄", | |
"4384874473945c5b70519ad5ace6305ef6b78c60c3c694add08a8b81899c4171" | |
), | |
}; | |
} | |
@Test | |
public void EncryptDecrypt() throws Exception { | |
for (int i = 0; i < cases.length; i++) { | |
byte[] raw = Hex.decodeHex(cases[i].raw.toCharArray()); | |
byte[] password = cases[i].password.getBytes(); | |
byte[] enc = Hex.decodeHex(cases[i].possibleEncrypted.toCharArray()); | |
byte[] encrypt = EndToEndEncryption.Encrypt(raw, password); | |
byte[] dec = EndToEndEncryption.Decrypt(encrypt, password); | |
byte[] dec2 = EndToEndEncryption.Decrypt(enc, password); | |
assertEquals(dec, raw); | |
assertEquals(dec2, raw); | |
System.out.printf("Test case: #%d Passed\n", i); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2019 The CovenantSQL Authors. | |
* | |
* 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 io.covenantsql.connector.util; | |
import java.security.*; | |
import javax.crypto.BadPaddingException; | |
import javax.crypto.Cipher; | |
import javax.crypto.spec.IvParameterSpec; | |
import javax.crypto.spec.SecretKeySpec; | |
public class EndToEndEncryption { | |
private static final SecureRandom random = new SecureRandom(); | |
private static final byte[] salt = new byte[]{ | |
(byte) 0x3f, (byte) 0xb8, (byte) 0x87, (byte) 0x7d, (byte) 0x37, (byte) 0xfd, (byte) 0xc0, (byte) 0x4e, | |
(byte) 0x4a, (byte) 0x47, (byte) 0x65, (byte) 0xEF, (byte) 0xb8, (byte) 0xab, (byte) 0x7d, (byte) 0x36 | |
}; | |
private static byte[] generateIV() { | |
byte[] ivBytes = new byte[16]; | |
random.nextBytes(ivBytes); | |
return ivBytes; | |
} | |
private static byte[] kdf(byte[] rawPass) { | |
MessageDigest sha = null; | |
MessageDigest final_sha = null; | |
byte[] key = new byte[16]; | |
try { | |
sha = MessageDigest.getInstance("SHA-256"); | |
final_sha = MessageDigest.getInstance("SHA-256"); | |
sha.update(rawPass); | |
sha.update(salt); | |
final_sha.update(sha.digest()); | |
System.arraycopy(final_sha.digest(), 0, key, 0, 16); | |
} catch (NoSuchAlgorithmException e) { | |
e.printStackTrace(); | |
} | |
return key; | |
} | |
public static byte[] Encrypt(final byte[] raw, | |
final byte[] key) { | |
try { | |
final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); | |
final int blockSize = cipher.getBlockSize(); | |
// create the key | |
final SecretKeySpec symKey = new SecretKeySpec(kdf(key), "AES"); | |
// generate random IV using block size (possibly create a method for | |
// this) | |
final byte[] ivData = new byte[blockSize]; | |
final SecureRandom rnd = SecureRandom.getInstance("SHA1PRNG"); | |
rnd.nextBytes(ivData); | |
final IvParameterSpec iv = new IvParameterSpec(ivData); | |
cipher.init(Cipher.ENCRYPT_MODE, symKey, iv); | |
final byte[] encryptedMessage = cipher.doFinal(raw); | |
// concatenate IV and encrypted message | |
final byte[] ivAndEncryptedMessage = new byte[ivData.length | |
+ encryptedMessage.length]; | |
System.arraycopy(ivData, 0, ivAndEncryptedMessage, 0, blockSize); | |
System.arraycopy(encryptedMessage, 0, ivAndEncryptedMessage, | |
blockSize, encryptedMessage.length); | |
return ivAndEncryptedMessage; | |
} catch (InvalidKeyException e) { | |
throw new IllegalArgumentException( | |
"key argument does not contain a valid AES key"); | |
} catch (GeneralSecurityException e) { | |
throw new IllegalStateException( | |
"Unexpected exception during encryption", e); | |
} | |
} | |
public static byte[] Decrypt(final byte[] enc, | |
final byte[] key) { | |
try { | |
final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); | |
final int blockSize = cipher.getBlockSize(); | |
// create the key | |
final SecretKeySpec symKey = new SecretKeySpec(kdf(key), "AES"); | |
// retrieve random IV from start of the received message | |
final byte[] ivData = new byte[blockSize]; | |
System.arraycopy(enc, 0, ivData, 0, blockSize); | |
final IvParameterSpec iv = new IvParameterSpec(ivData); | |
// retrieve the encrypted message itself | |
final byte[] encryptedMessage = new byte[enc.length | |
- blockSize]; | |
System.arraycopy(enc, blockSize, | |
encryptedMessage, 0, encryptedMessage.length); | |
cipher.init(Cipher.DECRYPT_MODE, symKey, iv); | |
return cipher.doFinal(encryptedMessage); | |
} catch (InvalidKeyException e) { | |
throw new IllegalArgumentException( | |
"key argument does not contain a valid AES key"); | |
} catch (BadPaddingException e) { | |
// you'd better know about padding oracle attacks | |
return null; | |
} catch (GeneralSecurityException e) { | |
throw new IllegalStateException( | |
"Unexpected exception during decryption", e); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# coding: utf-8 | |
import unittest | |
from pycovenantsql.e2ee import encrypt, decrypt, unpad, PaddingError | |
from binascii import hexlify, unhexlify | |
# Test cases for all implementations. | |
# Because iv is random, so Encrypted data is not always the same, | |
# but Decrypt(possibleEncrypted) will get raw. | |
cases = [ | |
{ | |
"raw": "11", | |
"pass": ";#K]As9C*6L", | |
"possibleEncrypted": "a372ea2c158a2f99d386e309db4355a659a7a8dd3986fd1d94f7604256061609", | |
}, | |
{ | |
"raw": "111282C128421286712857128C2128EF" + | |
"128B7671283C128571287512830128EC" + | |
"128391281A1312849128381281E1286A" + | |
"12871128621287A9D12857128C412886" + | |
"128FD12834128DA128F5", | |
"pass": "", | |
"possibleEncrypted": "1bfb6a7fda3e3eb1e14c9afd0baefe86" + | |
"c90979101f179db7e48a0fa7617881e8" + | |
"f752c59fb512bb86b8ed69c5644bf2dc" + | |
"30fbcd3bf79fb20342595c84fad00e46" + | |
"2fab3e51266492a3d5d085e650c1e619" + | |
"6278d7f5185c263440ec6fd940ffbb85", | |
}, | |
{ | |
"raw": "11", | |
"pass": "'K]\"#'pi/1/JD2", | |
"possibleEncrypted": "a83d152777ce3a1c0710b03676ae867c86ab0a47b3ca080f825683ac1079eb41", | |
}, | |
{ | |
"raw": "11111111111111111111111111111111", | |
"pass": "", | |
"possibleEncrypted": "7dda438c4256a63c62d6816617fcbf9c" + | |
"7773b9b4f87902b7253848ba2b0ed0ba" + | |
"f70a3ac976a835b7bc3008e9ba43da74", | |
}, | |
{ | |
"raw": "11111111111111111111111111111111", | |
"pass": "youofdas1312", | |
"possibleEncrypted": "cab07967cf377dbc010fbf5f84d12bcb" + | |
"6f8b188e6965738cf9007a671b4bfeb9" + | |
"f52257aac3808048c341dcaa1c125ca7", | |
}, | |
{ | |
"raw": "11111111111111111111111111", | |
"pass": "空のBottle😄", | |
"possibleEncrypted": "4384874473945c5b70519ad5ace6305ef6b78c60c3c694add08a8b81899c4171", | |
}, | |
] | |
class TestE2ee(unittest.TestCase): | |
def test_enc_dec(self): | |
i = 0 | |
for case in cases: | |
print("Case: #" + str(i)) | |
i += 1 | |
enc = encrypt(unhexlify(case["raw"]), case["pass"].encode()) | |
dec = decrypt(enc, case["pass"].encode()) | |
self.assertEqual(unhexlify(case["raw"]), dec) | |
dec2 = decrypt(unhexlify(case["possibleEncrypted"]), case["pass"].encode()) | |
self.assertEqual(unhexlify(case["raw"]), dec2) | |
def test_unpad_error(self): | |
self.assertEqual( | |
unpad(unhexlify("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa01")), | |
unhexlify("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") | |
) | |
self.assertEqual( | |
unpad(unhexlify("aaaaaaaaaaaaaaaaaaaaaaaaaaaa0202")), | |
unhexlify("aaaaaaaaaaaaaaaaaaaaaaaaaaaa") | |
) | |
self.assertRaisesRegex(PaddingError, "unexpected padding char", unpad, unhexlify("aaaaaaaaaaaaaaaaaaaaaaaaaaaa0102")) | |
self.assertRaisesRegex(PaddingError, "padding length > 16", unpad, unhexlify("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) | |
self.assertRaisesRegex(PaddingError, "empty input", unpad, unhexlify("")) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment