├── .gitignore ├── LICENSE ├── README.md ├── aesffx.go └── aesffx_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Olaoluwa Osuntokun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # perm-crypt 2 | A Golang implementation of the AES-FFX Format-Preserving Encryption Scheme 3 | 4 | # Installation 5 | ```bash 6 | $ go get github.com/roasbeef/perm-crypt 7 | ``` 8 | 9 | # Example Usage 10 | ```go 11 | key, _ := hex.DecodeString("2b7e151628aed2a6abf7158809cf4f3c") 12 | 13 | tweak := []byte("9876543210") 14 | 15 | plainString := "0123456789" 16 | 17 | ffx, _ := aesffx.NewCipher(10, key, tweak) 18 | 19 | cipher, _ := ffx.Encrypt(plainString) 20 | 21 | plain, _ := ffx.Decrypt(cipher) 22 | 23 | ``` 24 | ----- 25 | ##### WARNING: You probably shouldn't use this in a production environment. This lib was created primarily as yak-shaving for a research project. 26 | -------------------------------------------------------------------------------- /aesffx.go: -------------------------------------------------------------------------------- 1 | package aesffx 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "encoding/binary" 8 | "encoding/hex" 9 | "fmt" 10 | "math" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | const ( 16 | numRounds = 10 17 | ) 18 | 19 | // NewCipher creates a new cipher capable of encrypting and decrypting messages 20 | // using the AES-FFX mode for format-preserving encryption. 21 | func NewCipher(radix uint32, key, tweak []byte) (*FFXCipher, error) { 22 | var maxLength uint32 = (1 << 32) - 1 23 | 24 | if radix > 65536 { 25 | return nil, fmt.Errorf("radix must be between 2 and 2^16") 26 | } 27 | if len(key) != 16 { 28 | return nil, fmt.Errorf("key length must be exactly 16 bytes") 29 | } 30 | if uint32(len(tweak)) > maxLength { 31 | return nil, fmt.Errorf("tweak length must be smaller than (2^32) - 1") 32 | } 33 | 34 | var minLength uint32 = 2 35 | if radix >= 10 { 36 | minLength = 8 37 | } 38 | 39 | return &FFXCipher{ 40 | key: key, 41 | tweak: tweak, 42 | radix: radix, 43 | minLength: minLength, 44 | maxLength: maxLength, 45 | }, nil 46 | } 47 | 48 | // FFXCipher represents the parameters needed for AES-FFX. 49 | type FFXCipher struct { 50 | key []byte 51 | tweak []byte 52 | radix uint32 53 | 54 | minLength uint32 55 | maxLength uint32 56 | } 57 | 58 | // Encrypt encrypts the given plaintext, producing ciphertext output. 59 | func (f FFXCipher) Encrypt(src string) (string, error) { 60 | n := uint32(len(src)) 61 | l := split(uint32(n)) 62 | 63 | A := src[:l] 64 | B := src[l:] 65 | 66 | for i := 0; i < numRounds; i++ { 67 | fOut, err := f.feistelRound(n, f.tweak, i, B) 68 | if err != nil { 69 | return "", err 70 | } 71 | 72 | lmin := min(len(A), len(fOut)) 73 | 74 | C, err := blockAddition(lmin, int(f.radix), A, fOut) 75 | if err != nil { 76 | return "", nil 77 | } 78 | 79 | A = B 80 | B = C 81 | } 82 | cipher := A + B 83 | return cipher, nil 84 | } 85 | 86 | // Decrypt decrypts the given ciphertext, producing plaintext output. 87 | func (f FFXCipher) Decrypt(src string) (string, error) { 88 | n := uint32(len(src)) 89 | l := split(uint32(n)) 90 | 91 | A := src[:l] 92 | B := src[l:] 93 | 94 | for i := numRounds - 1; i > -1; i-- { 95 | C := B 96 | B = A 97 | 98 | fOut, err := f.feistelRound(n, f.tweak, i, B) 99 | if err != nil { 100 | return "", nil 101 | } 102 | 103 | lmin := min(len(C), len(fOut)) 104 | 105 | A, err = blockSubtraction(lmin, int(f.radix), C, fOut) 106 | if err != nil { 107 | return "", nil 108 | } 109 | } 110 | plain := A + B 111 | return plain, nil 112 | } 113 | 114 | // blockAddition computes the block-wise radix addition of x and y. 115 | func blockAddition(n, radix int, x, y string) (string, error) { 116 | xInt, err := strconv.ParseInt(x, radix, n*8) 117 | if err != nil { 118 | return "", err 119 | } 120 | yInt, err := strconv.ParseInt(y, radix, n*8) 121 | if err != nil { 122 | return "", err 123 | } 124 | 125 | blockSum := (xInt + yInt) % int64(math.Pow(float64(radix), float64(n))) 126 | 127 | out := strconv.FormatInt(blockSum, radix) 128 | if len(out) < n { 129 | out = strings.Repeat("0", n-len(out)) + out 130 | } 131 | return out, nil 132 | } 133 | 134 | // blockSubtraction computes the block-wise radix subtraction of x and y. 135 | func blockSubtraction(n, radix int, x, y string) (string, error) { 136 | xInt, err := strconv.ParseInt(x, radix, n*8) 137 | if err != nil { 138 | return "", err 139 | } 140 | yInt, err := strconv.ParseInt(y, radix, n*8) 141 | if err != nil { 142 | return "", err 143 | } 144 | 145 | diff := xInt - yInt 146 | mod := int64(math.Pow(float64(radix), float64(n))) 147 | blockDiff := diff % mod 148 | if blockDiff < 0 { 149 | blockDiff += mod 150 | } 151 | 152 | out := strconv.FormatInt(blockDiff, radix) 153 | if len(out) < n { 154 | out = strings.Repeat("0", n-len(out)) + out 155 | } 156 | return out, nil 157 | } 158 | 159 | // feistalRound runs the given block through the modified feistel network. 160 | func (f FFXCipher) feistelRound(msgLength uint32, tweak []byte, roundNum int, block string) (string, error) { 161 | t := len(tweak) 162 | beta := int(math.Ceil(float64(msgLength) / 2)) 163 | 164 | // b = ceil(ceil(beta * log_2(radix)) / 8) 165 | b := int(math.Ceil(math.Ceil(float64(beta)*math.Log2(float64(f.radix))) / 8)) 166 | 167 | // d = 4 * ceil(b/4) 168 | d := 4 * int(math.Ceil(float64(b)/4)) 169 | 170 | var m int 171 | if roundNum%2 == 0 { 172 | m = int(math.Floor(float64(msgLength) / 2)) 173 | } else { 174 | m = int(math.Ceil(float64(msgLength) / 2)) 175 | } 176 | 177 | // p <- [vers] | [method] | [addition] | [radix] | [rnds(n)] | [split(n)] | [n] | [t] 178 | p, err := generateP(msgLength, f.radix, t) 179 | if err != nil { 180 | return "", err 181 | } 182 | 183 | // q <- tweak | [0]^((−t−b−1) mod 16) | [roundNum] | [numradix(B)] 184 | err = generateQ(block, p, tweak, t, b, roundNum, f.radix) 185 | if err != nil { 186 | return "", err 187 | } 188 | 189 | // Y = CBC-MAC_k(P || Q) 190 | bigY, err := cbcMac(f.key, p.Bytes()) 191 | if err != nil { 192 | panic(err) 193 | } 194 | 195 | aes, err := aes.NewCipher(f.key) 196 | if err != nil { 197 | return "", nil 198 | } 199 | 200 | // Y <- first d+4 bytes of (Y | AESK(Y XOR [1]16) | AESK(Y XOR [2]16) | AESK(Y XOR [3]16)...) 201 | var yTemp bytes.Buffer 202 | c := bytes.NewBuffer(make([]byte, 16)) 203 | i := 0 204 | yTemp.Write(bigY) 205 | for yTemp.Len() < (d + 4) { 206 | h, err := hex.DecodeString(strings.Repeat("0"+strconv.Itoa(i), 16)) 207 | if err != nil { 208 | return "", nil 209 | } 210 | aes.Encrypt(c.Bytes(), xorBytes(bigY, h)) 211 | yTemp.Write(c.Bytes()) 212 | 213 | i++ 214 | c.Reset() 215 | } 216 | 217 | // z = y mod r^m 218 | y := binary.BigEndian.Uint64(yTemp.Bytes()) 219 | z := y % uint64(math.Pow(float64(f.radix), float64(m))) 220 | 221 | fOut := strconv.FormatUint(z, int(f.radix)) 222 | // TODO(roasbeef): Factor out into padding funciton 223 | if len(fOut) < beta { 224 | fOut = strings.Repeat("0", beta-len(fOut)) + fOut 225 | } 226 | return fOut, nil 227 | } 228 | 229 | // split calculates the index to split the input string for our maximally 230 | // balanced Feistel rounds. 231 | func split(n uint32) uint32 { 232 | return uint32(math.Floor(float64(n) / 2)) 233 | } 234 | 235 | // cbcMac computes the AES-CBC-MAC of the msg with the given key. 236 | func cbcMac(key, msg []byte) ([]byte, error) { 237 | if len(msg)%16 != 0 { 238 | return nil, fmt.Errorf("message length must be a multiple of 16, got %v", len(msg)) 239 | } 240 | if len(key) != 16 { 241 | return nil, fmt.Errorf("key length must be exactly 128-bits") 242 | } 243 | 244 | // Create a new aes cipher from our key. 245 | aesBlock, err := aes.NewCipher(key) 246 | if err != nil { 247 | return nil, err 248 | } 249 | 250 | // Initialize aes in CBC mode with a zero IV. 251 | y := make([]byte, 16, 16) 252 | cbc := cipher.NewCBCEncrypter(aesBlock, y) 253 | 254 | for i := 0; i < len(msg); i += 16 { 255 | x := msg[i:(i + 16)] 256 | cbc.CryptBlocks(y, x) 257 | } 258 | return y, nil 259 | } 260 | 261 | // xorBytes returns a new byte slice which is the result of XOR'ing each byte 262 | // amongst the passed arguments. 263 | func xorBytes(x, y []byte) []byte { 264 | out := make([]byte, len(x)) 265 | for i := 0; i < len(x); i++ { 266 | out[i] = y[i] ^ x[i] 267 | } 268 | return out 269 | } 270 | 271 | // min returns the minimum of x and y. 272 | func min(x, y int) int { 273 | if x < y { 274 | return x 275 | } 276 | return y 277 | } 278 | 279 | // generateP creates the first half of the IV our feistel round. 280 | // This function returns a bytes.Buffer in so Q can easily be concatenated to 281 | // it. 282 | func generateP(mlen uint32, radix uint32, tweaklen int) (*bytes.Buffer, error) { 283 | var p bytes.Buffer 284 | p.Write([]byte{0x01}) // version 285 | p.Write([]byte{0x02}) // method 286 | p.Write([]byte{0x01}) // addition 287 | p.Write([]byte{0x00}) // 0 byte prefix to force 3 bytes 288 | err := binary.Write(&p, binary.BigEndian, uint16(radix)) // write 2 bytes of radix 289 | if err != nil { 290 | return nil, err 291 | } 292 | 293 | p.Write([]byte{0x0a}) // number of rounds is 10 294 | err = binary.Write(&p, binary.BigEndian, uint8(split(mlen))) // split 295 | if err != nil { 296 | return nil, err 297 | } 298 | 299 | err = binary.Write(&p, binary.BigEndian, mlen) 300 | if err != nil { 301 | return nil, err 302 | } 303 | 304 | err = binary.Write(&p, binary.BigEndian, uint32(tweaklen)) 305 | if err != nil { 306 | return nil, err 307 | } 308 | 309 | return &p, nil 310 | } 311 | 312 | func generateQ(block string, buf *bytes.Buffer, tweak []byte, tlen int, blockLen int, i int, radix uint32) error { 313 | buf.Write(tweak) 314 | numPads := ((-1 * tlen) - blockLen - 1) % 16 315 | if numPads < 0 { 316 | numPads += 16 317 | } 318 | 319 | zeroPad, err := hex.DecodeString(strings.Repeat("00", numPads)) 320 | if err != nil { 321 | return err 322 | } 323 | 324 | buf.Write(zeroPad) 325 | err = binary.Write(buf, binary.BigEndian, uint8(i)) 326 | if err != nil { 327 | return err 328 | } 329 | 330 | var bBuffer bytes.Buffer 331 | radixBlock, err := strconv.ParseUint(block, int(radix), blockLen*8) 332 | if err != nil { 333 | return err 334 | } 335 | 336 | err = binary.Write(&bBuffer, binary.BigEndian, radixBlock) 337 | if err != nil { 338 | return err 339 | } 340 | 341 | buf.Write(bBuffer.Bytes()[bBuffer.Len()-blockLen:]) 342 | 343 | return nil 344 | } 345 | -------------------------------------------------------------------------------- /aesffx_test.go: -------------------------------------------------------------------------------- 1 | package aesffx 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | ) 7 | 8 | // Test vectors taken from: 9 | // * http://csrc.nist.gov/groups/ST/toolkit/BCM/documents/proposedmodes/ffx/aes-ffx-vectors.txt 10 | 11 | func TestVector1(t *testing.T) { 12 | key, err := hex.DecodeString("2b7e151628aed2a6abf7158809cf4f3c") 13 | if err != nil { 14 | t.Fatalf("Unable to decode hex key: %v", key) 15 | } 16 | 17 | tweak := []byte("9876543210") 18 | plainString := "0123456789" 19 | 20 | aes, err := NewCipher(10, key, tweak) 21 | if err != nil { 22 | t.Fatalf("Unable to create cipher: %v", err) 23 | } 24 | 25 | cipher, err := aes.Encrypt(plainString) 26 | if err != nil { 27 | t.Fatalf("%v", err) 28 | } 29 | if cipher != "6124200773" { 30 | t.Fatalf("Encryption was incorrect. Need %v, got %v", 31 | "6124200773", cipher) 32 | } 33 | 34 | plain, err := aes.Decrypt(cipher) 35 | if err != nil { 36 | t.Fatalf("%v", err) 37 | } 38 | if plain != plainString { 39 | t.Fatalf("Decryption unsuccessful. Need %v, got %v", 40 | plainString, plain) 41 | } 42 | } 43 | 44 | func TestVector2(t *testing.T) { 45 | key, err := hex.DecodeString("2b7e151628aed2a6abf7158809cf4f3c") 46 | if err != nil { 47 | t.Fatalf("Unable to decode hex key: %v", key) 48 | 49 | } 50 | 51 | var tweak []byte 52 | plainString := "0123456789" 53 | 54 | aes, err := NewCipher(10, key, tweak) 55 | if err != nil { 56 | t.Fatalf("Unable to create cipher: %v", err) 57 | } 58 | 59 | cipher, err := aes.Encrypt(plainString) 60 | if err != nil { 61 | t.Fatalf("%v", err) 62 | } 63 | if cipher != "2433477484" { 64 | t.Fatalf("Encryption was incorrect. Need %v, got %v", 65 | "2433477484", cipher) 66 | } 67 | 68 | plain, err := aes.Decrypt(cipher) 69 | if err != nil { 70 | t.Fatalf("%v", err) 71 | } 72 | if plain != plainString { 73 | t.Fatalf("Decryption unsuccessful. Need %v, got %v", 74 | plainString, plain) 75 | } 76 | } 77 | 78 | func TestVector3(t *testing.T) { 79 | key, err := hex.DecodeString("2b7e151628aed2a6abf7158809cf4f3c") 80 | if err != nil { 81 | t.Fatalf("Unable to decode hex key: %v", key) 82 | 83 | } 84 | 85 | tweak := []byte("2718281828") 86 | plainString := "314159" 87 | 88 | aes, err := NewCipher(10, key, tweak) 89 | if err != nil { 90 | t.Fatalf("Unable to create cipher: %v", err) 91 | } 92 | 93 | cipher, err := aes.Encrypt(plainString) 94 | if err != nil { 95 | t.Fatalf("%v", err) 96 | } 97 | if cipher != "535005" { 98 | t.Fatalf("Encryption was incorrect. Need %v, got %v", 99 | "535005", cipher) 100 | } 101 | 102 | plain, err := aes.Decrypt(cipher) 103 | if err != nil { 104 | t.Fatalf("%v", err) 105 | } 106 | if plain != plainString { 107 | t.Fatalf("Decryption unsuccessful. Need %v, got %v", 108 | plainString, plain) 109 | } 110 | } 111 | 112 | func TestVector4(t *testing.T) { 113 | key, err := hex.DecodeString("2b7e151628aed2a6abf7158809cf4f3c") 114 | if err != nil { 115 | t.Fatalf("Unable to decode hex key: %v", key) 116 | 117 | } 118 | tweak := []byte("7777777") 119 | plainString := "999999999" 120 | 121 | aes, err := NewCipher(10, key, tweak) 122 | if err != nil { 123 | t.Fatalf("Unable to create cipher: %v", err) 124 | } 125 | 126 | cipher, err := aes.Encrypt(plainString) 127 | if err != nil { 128 | t.Fatalf("%v", err) 129 | } 130 | if cipher != "658229573" { 131 | t.Fatalf("Encryption was incorrect. Need %v, got %v", 132 | "658229573", cipher) 133 | } 134 | 135 | plain, err := aes.Decrypt(cipher) 136 | if err != nil { 137 | t.Fatalf("%v", err) 138 | } 139 | if plain != plainString { 140 | t.Fatalf("Decryption unsuccessful. Need %v, got %v", 141 | plainString, plain) 142 | } 143 | } 144 | --------------------------------------------------------------------------------