paygate-ruby 0.1.6 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/paygate/aes.rb CHANGED
@@ -1,150 +1,150 @@
1
- module Paygate
2
- class Aes
3
- # Pre-computed multiplicative inverse in GF(2^8)
4
- S_BOX = [0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
5
- 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
6
- 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
7
- 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
8
- 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
9
- 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
10
- 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
11
- 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
12
- 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
13
- 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
14
- 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
15
- 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
16
- 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
17
- 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
18
- 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
19
- 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16].freeze
20
-
21
- # Round Constant used for the Key Expansion [1st col is 2^(r-1) in GF(2^8)]
22
- R_CON = [[0x00, 0x00, 0x00, 0x00],
23
- [0x01, 0x00, 0x00, 0x00],
24
- [0x02, 0x00, 0x00, 0x00],
25
- [0x04, 0x00, 0x00, 0x00],
26
- [0x08, 0x00, 0x00, 0x00],
27
- [0x10, 0x00, 0x00, 0x00],
28
- [0x20, 0x00, 0x00, 0x00],
29
- [0x40, 0x00, 0x00, 0x00],
30
- [0x80, 0x00, 0x00, 0x00],
31
- [0x1b, 0x00, 0x00, 0x00],
32
- [0x36, 0x00, 0x00, 0x00] ].freeze
33
-
34
- # Block size (in words): no of columns in state (fixed for AES)
35
- CIPHER_BLOCK_SIZE = 4.freeze
36
-
37
- # AES Cipher function: encrypt 'input' state with Rijndael algorithm
38
- # applies Nr rounds (10/12/14) using key schedule w for 'add round key' stage
39
- # @param int[] input 16-byte (128-bit) input state array
40
- # @param int[][] w Key schedule as 2D byte-array (Nr+1 x Nb bytes)
41
- # @returns int[] Encrypted output state array
42
- def self.cipher(input, w)
43
- nr = w.length / CIPHER_BLOCK_SIZE - 1 # no of rounds: 10/12/14 for 128/192/256-bit keys
44
- state = [[],[],[],[]] # initialize 4x4 byte-array 'state' with input
45
-
46
- (0...(4 * CIPHER_BLOCK_SIZE)).each do |i|
47
- state[i % 4][(i / 4.0).floor] = input[i]
48
- end
49
-
50
- state = add_round_key(state, w, 0)
51
- (1...nr).each do |round|
52
- state = sub_bytes(state)
53
- state = shift_rows(state)
54
- state = mix_columns(state)
55
- state = add_round_key(state, w, round)
56
- end
57
-
58
- state = sub_bytes(state)
59
- state = shift_rows(state)
60
- state = add_round_key(state, w, nr)
61
-
62
- output = []
63
- (0...(4 * CIPHER_BLOCK_SIZE)).each { |i| output[i] = state[i % 4][(i / 4.0).floor] }
64
- output
65
- end
66
-
67
- # Perform Key Expansion to generate a Key Schedule
68
- #
69
- # @param int[] key Key as 16/24/32-byte array
70
- # @returns int[][] Expanded key schedule as 2D byte-array (Nr+1 x Nb bytes)
71
- def self.key_expansion(key)
72
- nk = key.length / 4 # key length (in words): 4/6/8 for 128/192/256-bit keys
73
- nr = nk + 6 # no of rounds: 10/12/14 for 128/192/256-bit keys
74
-
75
- w = []
76
- temp = []
77
- 0.upto(nk - 1){ |i| w[i] = [key[4 * i], key[4 * i + 1], key[4 * i + 2], key[4 * i + 3]] }
78
- (nk...(CIPHER_BLOCK_SIZE * (nr + 1))).each do |i|
79
- w[i] = []
80
- 0.upto(3){ |t| temp[t] = w[i - 1][t] }
81
- if i % nk == 0
82
- temp = sub_word(rotate_word(temp))
83
- 0.upto(3){ |t| temp[t] ^= R_CON[i / nk][t]}
84
- elsif nk > 6 && i % nk == 4
85
- temp = sub_word(temp)
86
- end
87
- 0.upto(3){ |t| w[i][t] = w[i - nk][t] ^ temp[t] }
88
- end
89
- w
90
- end
91
-
92
- # apply SBox to state
93
- def self.sub_bytes(state)
94
- 0.upto(3) do |r|
95
- 0.upto(CIPHER_BLOCK_SIZE - 1){ |c| state[r][c] = S_BOX[state[r][c]] }
96
- end
97
- state
98
- end
99
-
100
- # shift row r of state S left by r bytes
101
- def self.shift_rows(state)
102
- t = []
103
- 1.upto(3) do |r|
104
- 0.upto(3){ |c| t[c] = state[r][(c + r) % CIPHER_BLOCK_SIZE] } # shift into temp copy
105
- 0.upto(3){ |c| state[r][c] = t[c] } # and copy back
106
- end # note that this will work for nb = 4,5,6, but not 7,8 (always 4 for AES):
107
- state # see asmaes.sourceforge.net/rijndael/rijndaelImplementation.pdf
108
- end
109
-
110
- # combine bytes of each col of state S
111
- def self.mix_columns(state)
112
- 0.upto(3) do |c|
113
- a=[] # 'a' is a copy of the current column from 'state'
114
- b=[] # 'b' is a•{02} in GF(2^8)
115
- 0.upto(3) do |i|
116
- a[i] = state[i][c]
117
- b[i] = (state[i][c] & 0x80 == 0) ? (state[i][c] << 1) ^ 0x011b : state[i][c] << 1
118
- end
119
- # a[n] ^ b[n] is a•{03} in GF(2^8)
120
- state[0][c] = b[0] ^ a[1] ^ b[1] ^ a[2] ^ a[3] # 2*a0 + 3*a1 + a2 + a3
121
- state[1][c] = a[0] ^ b[1] ^ a[2] ^ b[2] ^ a[3] # a0 * 2*a1 + 3*a2 + a3
122
- state[2][c] = a[0] ^ a[1] ^ b[2] ^ a[3] ^ b[3] # a0 + a1 + 2*a2 + 3*a3
123
- state[3][c] = a[0] ^ b[0] ^ a[1] ^ a[2] ^ b[3] # 3*a0 + a1 + a2 + 2*a3
124
- end
125
- state
126
- end
127
-
128
- # xor Round Key into state S
129
- def self.add_round_key(state, w, rnd)
130
- 0.upto(3) do |r|
131
- 0.upto(CIPHER_BLOCK_SIZE - 1){ |c| state[r][c] ^= w[rnd * 4 + c][r]}
132
- end
133
- state
134
- end
135
-
136
- # apply SBox to 4-byte word w
137
- def self.sub_word(word)
138
- 0.upto(3){ |i| word[i] = S_BOX[word[i]] }
139
- word
140
- end
141
-
142
- # rotate 4-byte word w left by one byte
143
- def self.rotate_word(word)
144
- tmp = word[0]
145
- 0.upto(2){ |i| word[i] = word[i + 1] }
146
- word[3] = tmp
147
- word
148
- end
149
- end
150
- end
1
+ module Paygate
2
+ class Aes
3
+ # Pre-computed multiplicative inverse in GF(2^8)
4
+ S_BOX = [0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
5
+ 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
6
+ 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
7
+ 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
8
+ 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
9
+ 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
10
+ 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
11
+ 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
12
+ 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
13
+ 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
14
+ 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
15
+ 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
16
+ 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
17
+ 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
18
+ 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
19
+ 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16].freeze
20
+
21
+ # Round Constant used for the Key Expansion [1st col is 2^(r-1) in GF(2^8)]
22
+ R_CON = [[0x00, 0x00, 0x00, 0x00],
23
+ [0x01, 0x00, 0x00, 0x00],
24
+ [0x02, 0x00, 0x00, 0x00],
25
+ [0x04, 0x00, 0x00, 0x00],
26
+ [0x08, 0x00, 0x00, 0x00],
27
+ [0x10, 0x00, 0x00, 0x00],
28
+ [0x20, 0x00, 0x00, 0x00],
29
+ [0x40, 0x00, 0x00, 0x00],
30
+ [0x80, 0x00, 0x00, 0x00],
31
+ [0x1b, 0x00, 0x00, 0x00],
32
+ [0x36, 0x00, 0x00, 0x00] ].freeze
33
+
34
+ # Block size (in words): no of columns in state (fixed for AES)
35
+ CIPHER_BLOCK_SIZE = 4.freeze
36
+
37
+ # AES Cipher function: encrypt 'input' state with Rijndael algorithm
38
+ # applies Nr rounds (10/12/14) using key schedule w for 'add round key' stage
39
+ # @param int[] input 16-byte (128-bit) input state array
40
+ # @param int[][] w Key schedule as 2D byte-array (Nr+1 x Nb bytes)
41
+ # @returns int[] Encrypted output state array
42
+ def self.cipher(input, w)
43
+ nr = w.length / CIPHER_BLOCK_SIZE - 1 # no of rounds: 10/12/14 for 128/192/256-bit keys
44
+ state = [[],[],[],[]] # initialize 4x4 byte-array 'state' with input
45
+
46
+ (0...(4 * CIPHER_BLOCK_SIZE)).each do |i|
47
+ state[i % 4][(i / 4.0).floor] = input[i]
48
+ end
49
+
50
+ state = add_round_key(state, w, 0)
51
+ (1...nr).each do |round|
52
+ state = sub_bytes(state)
53
+ state = shift_rows(state)
54
+ state = mix_columns(state)
55
+ state = add_round_key(state, w, round)
56
+ end
57
+
58
+ state = sub_bytes(state)
59
+ state = shift_rows(state)
60
+ state = add_round_key(state, w, nr)
61
+
62
+ output = []
63
+ (0...(4 * CIPHER_BLOCK_SIZE)).each { |i| output[i] = state[i % 4][(i / 4.0).floor] }
64
+ output
65
+ end
66
+
67
+ # Perform Key Expansion to generate a Key Schedule
68
+ #
69
+ # @param int[] key Key as 16/24/32-byte array
70
+ # @returns int[][] Expanded key schedule as 2D byte-array (Nr+1 x Nb bytes)
71
+ def self.key_expansion(key)
72
+ nk = key.length / 4 # key length (in words): 4/6/8 for 128/192/256-bit keys
73
+ nr = nk + 6 # no of rounds: 10/12/14 for 128/192/256-bit keys
74
+
75
+ w = []
76
+ temp = []
77
+ 0.upto(nk - 1){ |i| w[i] = [key[4 * i], key[4 * i + 1], key[4 * i + 2], key[4 * i + 3]] }
78
+ (nk...(CIPHER_BLOCK_SIZE * (nr + 1))).each do |i|
79
+ w[i] = []
80
+ 0.upto(3){ |t| temp[t] = w[i - 1][t] }
81
+ if i % nk == 0
82
+ temp = sub_word(rotate_word(temp))
83
+ 0.upto(3){ |t| temp[t] ^= R_CON[i / nk][t]}
84
+ elsif nk > 6 && i % nk == 4
85
+ temp = sub_word(temp)
86
+ end
87
+ 0.upto(3){ |t| w[i][t] = w[i - nk][t] ^ temp[t] }
88
+ end
89
+ w
90
+ end
91
+
92
+ # apply SBox to state
93
+ def self.sub_bytes(state)
94
+ 0.upto(3) do |r|
95
+ 0.upto(CIPHER_BLOCK_SIZE - 1){ |c| state[r][c] = S_BOX[state[r][c]] }
96
+ end
97
+ state
98
+ end
99
+
100
+ # shift row r of state S left by r bytes
101
+ def self.shift_rows(state)
102
+ t = []
103
+ 1.upto(3) do |r|
104
+ 0.upto(3){ |c| t[c] = state[r][(c + r) % CIPHER_BLOCK_SIZE] } # shift into temp copy
105
+ 0.upto(3){ |c| state[r][c] = t[c] } # and copy back
106
+ end # note that this will work for nb = 4,5,6, but not 7,8 (always 4 for AES):
107
+ state # see asmaes.sourceforge.net/rijndael/rijndaelImplementation.pdf
108
+ end
109
+
110
+ # combine bytes of each col of state S
111
+ def self.mix_columns(state)
112
+ 0.upto(3) do |c|
113
+ a=[] # 'a' is a copy of the current column from 'state'
114
+ b=[] # 'b' is a•{02} in GF(2^8)
115
+ 0.upto(3) do |i|
116
+ a[i] = state[i][c]
117
+ b[i] = (state[i][c] & 0x80 == 0) ? (state[i][c] << 1) ^ 0x011b : state[i][c] << 1
118
+ end
119
+ # a[n] ^ b[n] is a•{03} in GF(2^8)
120
+ state[0][c] = b[0] ^ a[1] ^ b[1] ^ a[2] ^ a[3] # 2*a0 + 3*a1 + a2 + a3
121
+ state[1][c] = a[0] ^ b[1] ^ a[2] ^ b[2] ^ a[3] # a0 * 2*a1 + 3*a2 + a3
122
+ state[2][c] = a[0] ^ a[1] ^ b[2] ^ a[3] ^ b[3] # a0 + a1 + 2*a2 + 3*a3
123
+ state[3][c] = a[0] ^ b[0] ^ a[1] ^ a[2] ^ b[3] # 3*a0 + a1 + a2 + 2*a3
124
+ end
125
+ state
126
+ end
127
+
128
+ # xor Round Key into state S
129
+ def self.add_round_key(state, w, rnd)
130
+ 0.upto(3) do |r|
131
+ 0.upto(CIPHER_BLOCK_SIZE - 1){ |c| state[r][c] ^= w[rnd * 4 + c][r]}
132
+ end
133
+ state
134
+ end
135
+
136
+ # apply SBox to 4-byte word w
137
+ def self.sub_word(word)
138
+ 0.upto(3){ |i| word[i] = S_BOX[word[i]] }
139
+ word
140
+ end
141
+
142
+ # rotate 4-byte word w left by one byte
143
+ def self.rotate_word(word)
144
+ tmp = word[0]
145
+ 0.upto(2){ |i| word[i] = word[i + 1] }
146
+ word[3] = tmp
147
+ word
148
+ end
149
+ end
150
+ end
@@ -1,133 +1,133 @@
1
- require 'base64'
2
-
3
- module Paygate
4
- class AesCtr
5
-
6
- # Encrypt a text using AES encryption in Counter mode of operation
7
- #
8
- # Unicode multi-byte character safe
9
- #
10
- # @param string plaintext Source text to be encrypted
11
- # @param string password The password to use to generate a key
12
- # @param int num_bits Number of bits to be used in the key (128, 192, or 256)
13
- # @returns string Encrypted text
14
- def self.encrypt(plaintext, password, num_bits)
15
- block_size = 16 # block size fixed at 16 bytes / 128 bits (Nb=4) for AES
16
- return '' unless num_bits.in?([128, 192, 256])
17
-
18
- # use AES itself to encrypt password to get cipher key (using plain password as source for key
19
- # expansion) - gives us well encrypted key (though hashed key might be preferred for prod'n use)
20
- num_bytes = num_bits / 8 # no bytes in key (16/24/32)
21
- pw_bytes = []
22
- 0.upto(num_bytes - 1){ |i| pw_bytes[i] = password.bytes.to_a[i] & 0xff || 0} # use 1st 16/24/32 chars of password for key #warn
23
- key = Aes.cipher(pw_bytes, Aes.key_expansion(pw_bytes)) # gives us 16-byte key
24
- key = key + key[0, num_bytes - 16] # expand key to 16/24/32 bytes long
25
-
26
- # initialise 1st 8 bytes of counter block with nonce (NIST SP800-38A §B.2): [0-1] = millisec,
27
- # [2-3] = random, [4-7] = seconds, together giving full sub-millisec uniqueness up to Feb 2106
28
- counter_block = []
29
- nonce = Time.now.to_i
30
- nonce_ms = nonce % 1000
31
- nonce_sec = (nonce / 1000.0).floor
32
- nonce_rand = (rand() * 0xffff).floor
33
- 0.upto(1){ |i| counter_block[i] = urs(nonce_ms, i * 8) & 0xff }
34
- 0.upto(1){ |i| counter_block[i + 2] = urs(nonce_rand, i * 8) & 0xff }
35
- 0.upto(3){ |i| counter_block[i + 4] = urs(nonce_sec, i * 8) & 0xff }
36
-
37
- # and convert it to a string to go on the front of the ciphertext
38
- ctr_text = ''
39
- 0.upto(7){ |i| ctr_text += counter_block[i].chr }
40
-
41
- # generate key schedule - an expansion of the key into distinct Key Rounds for each round
42
- key_schedule = Aes.key_expansion(key)
43
- block_count = (plaintext.length / block_size.to_f).ceil
44
-
45
- cipher_text = []
46
- 0.upto(block_count - 1) do |b|
47
- # set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes)
48
- # done in two stages for 32-bit ops: using two words allows us to go past 2^32 blocks (68GB)
49
- 0.upto(3){ |c| counter_block[15 - c] = urs(b, c * 8) & 0xff }
50
- 0.upto(3){ |c| counter_block[15 - c - 4] = urs(b / 0x100000000, c * 8) }
51
-
52
- cipher_cntr = Aes.cipher(counter_block, key_schedule) # -- encrypt counter block --
53
- # block size is reduced on final block
54
- block_length = b < block_count - 1 ? block_size : (plaintext.length - 1) % block_size + 1
55
- cipher_char = []
56
- 0.upto(block_length - 1) do |i|
57
- cipher_char[i] = (cipher_cntr[i] ^ plaintext.bytes.to_a[b * block_size + i]).chr
58
- end
59
- cipher_text[b] = cipher_char.join
60
- end
61
-
62
- cipher_text = ctr_text + cipher_text.join
63
- cipher_text = Base64.encode64(cipher_text).gsub(/\n/, '') + "\n"; # encode in base64
64
- end
65
-
66
- # Decrypt a text encrypted by AES in counter mode of operation
67
- #
68
- # @param string ciphertext Source text to be encrypted
69
- # @param string password The password to use to generate a key
70
- # @param int nBits Number of bits to be used in the key (128, 192, or 256)
71
- # @returns string
72
- # Decrypted text
73
- def self.decrypt(ciphertext, password, nBits)
74
- blockSize = 16 # block size fixed at 16 bytes / 128 bits (Nb=4) for AES
75
- return '' unless(nBits==128 || nBits==192 || nBits==256)
76
- ciphertext = Base64.decode64(ciphertext);
77
-
78
- nBytes = nBits/8 # no bytes in key (16/24/32)
79
- pwBytes = []
80
- 0.upto(nBytes-1){|i| pwBytes[i] = password.bytes.to_a[i] & 0xff || 0}
81
- key = Aes.cipher(pwBytes, Aes.key_expansion(pwBytes)) # gives us 16-byte key
82
- key = key.concat(key.slice(0, nBytes-16)) # expand key to 16/24/32 bytes long
83
- # recover nonce from 1st 8 bytes of ciphertext
84
- counterBlock = []
85
- ctrTxt = ciphertext[0,8]
86
- 0.upto(7){|i| counterBlock[i] = ctrTxt.bytes.to_a[i]}
87
-
88
- #generate key Schedule
89
- keySchedule = Aes.key_expansion(key);
90
-
91
- # separate ciphertext into blocks (skipping past initial 8 bytes)
92
- nBlocks = ((ciphertext.length-8)/blockSize.to_f).ceil
93
- ct=[]
94
- 0.upto(nBlocks-1){|b|ct[b] = ciphertext[8+b*blockSize, 16]}
95
-
96
- ciphertext = ct; # ciphertext is now array of block-length strings
97
-
98
- # plaintext will get generated block-by-block into array of block-length strings
99
- plaintxt = [];
100
- 0.upto(nBlocks-1) do |b|
101
- 0.upto(3){|c| counterBlock[15-c] = urs(b,c*8) & 0xff}
102
- 0.upto(3){|c| counterBlock[15-c-4] = urs((b+1)/(0x100000000-1),c*8) & 0xff}
103
- cipherCntr = Aes.cipher(counterBlock, keySchedule) # encrypt counter block
104
- plaintxtByte = []
105
- 0.upto(ciphertext[b].length - 1) do |i|
106
- # -- xor plaintxt with ciphered counter byte-by-byte --
107
- plaintxtByte[i] = (cipherCntr[i] ^ ciphertext[b].bytes.to_a[i]).chr;
108
- end
109
- plaintxt[b] = plaintxtByte.join('')
110
- end
111
- plaintext = plaintxt.join('')
112
- end
113
-
114
- private
115
-
116
- # Unsigned right shift function, since Ruby has neither >>> operator nor unsigned ints
117
- #
118
- # @param a number to be shifted (32-bit integer)
119
- # @param b number of bits to shift a to the right (0..31)
120
- # @return a right-shifted and zero-filled by b bits
121
- def self.urs(a, b)
122
- a &= 0xffffffff
123
- b &= 0x1f
124
- if a & 0x80000000 && b > 0 # if left-most bit set
125
- a = ((a >> 1) & 0x7fffffff) # right-shift one bit & clear left-most bit
126
- a = a >> (b - 1) # remaining right-shifts
127
- else # otherwise
128
- a = (a >> b); # use normal right-shift
129
- end
130
- a
131
- end
132
- end
133
- end
1
+ require 'base64'
2
+
3
+ module Paygate
4
+ class AesCtr
5
+
6
+ # Encrypt a text using AES encryption in Counter mode of operation
7
+ #
8
+ # Unicode multi-byte character safe
9
+ #
10
+ # @param string plaintext Source text to be encrypted
11
+ # @param string password The password to use to generate a key
12
+ # @param int num_bits Number of bits to be used in the key (128, 192, or 256)
13
+ # @returns string Encrypted text
14
+ def self.encrypt(plaintext, password, num_bits)
15
+ block_size = 16 # block size fixed at 16 bytes / 128 bits (Nb=4) for AES
16
+ return '' unless num_bits.in?([128, 192, 256])
17
+
18
+ # use AES itself to encrypt password to get cipher key (using plain password as source for key
19
+ # expansion) - gives us well encrypted key (though hashed key might be preferred for prod'n use)
20
+ num_bytes = num_bits / 8 # no bytes in key (16/24/32)
21
+ pw_bytes = []
22
+ 0.upto(num_bytes - 1){ |i| pw_bytes[i] = password.bytes.to_a[i] & 0xff || 0} # use 1st 16/24/32 chars of password for key #warn
23
+ key = Aes.cipher(pw_bytes, Aes.key_expansion(pw_bytes)) # gives us 16-byte key
24
+ key = key + key[0, num_bytes - 16] # expand key to 16/24/32 bytes long
25
+
26
+ # initialise 1st 8 bytes of counter block with nonce (NIST SP800-38A §B.2): [0-1] = millisec,
27
+ # [2-3] = random, [4-7] = seconds, together giving full sub-millisec uniqueness up to Feb 2106
28
+ counter_block = []
29
+ nonce = Time.now.to_i
30
+ nonce_ms = nonce % 1000
31
+ nonce_sec = (nonce / 1000.0).floor
32
+ nonce_rand = (rand() * 0xffff).floor
33
+ 0.upto(1){ |i| counter_block[i] = urs(nonce_ms, i * 8) & 0xff }
34
+ 0.upto(1){ |i| counter_block[i + 2] = urs(nonce_rand, i * 8) & 0xff }
35
+ 0.upto(3){ |i| counter_block[i + 4] = urs(nonce_sec, i * 8) & 0xff }
36
+
37
+ # and convert it to a string to go on the front of the ciphertext
38
+ ctr_text = ''
39
+ 0.upto(7){ |i| ctr_text += counter_block[i].chr }
40
+
41
+ # generate key schedule - an expansion of the key into distinct Key Rounds for each round
42
+ key_schedule = Aes.key_expansion(key)
43
+ block_count = (plaintext.length / block_size.to_f).ceil
44
+
45
+ cipher_text = []
46
+ 0.upto(block_count - 1) do |b|
47
+ # set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes)
48
+ # done in two stages for 32-bit ops: using two words allows us to go past 2^32 blocks (68GB)
49
+ 0.upto(3){ |c| counter_block[15 - c] = urs(b, c * 8) & 0xff }
50
+ 0.upto(3){ |c| counter_block[15 - c - 4] = urs(b / 0x100000000, c * 8) }
51
+
52
+ cipher_cntr = Aes.cipher(counter_block, key_schedule) # -- encrypt counter block --
53
+ # block size is reduced on final block
54
+ block_length = b < block_count - 1 ? block_size : (plaintext.length - 1) % block_size + 1
55
+ cipher_char = []
56
+ 0.upto(block_length - 1) do |i|
57
+ cipher_char[i] = (cipher_cntr[i] ^ plaintext.bytes.to_a[b * block_size + i]).chr
58
+ end
59
+ cipher_text[b] = cipher_char.join
60
+ end
61
+
62
+ cipher_text = ctr_text + cipher_text.join
63
+ cipher_text = Base64.encode64(cipher_text).gsub(/\n/, '') + "\n"; # encode in base64
64
+ end
65
+
66
+ # Decrypt a text encrypted by AES in counter mode of operation
67
+ #
68
+ # @param string ciphertext Source text to be encrypted
69
+ # @param string password The password to use to generate a key
70
+ # @param int nBits Number of bits to be used in the key (128, 192, or 256)
71
+ # @returns string
72
+ # Decrypted text
73
+ def self.decrypt(ciphertext, password, nBits)
74
+ blockSize = 16 # block size fixed at 16 bytes / 128 bits (Nb=4) for AES
75
+ return '' unless(nBits==128 || nBits==192 || nBits==256)
76
+ ciphertext = Base64.decode64(ciphertext);
77
+
78
+ nBytes = nBits/8 # no bytes in key (16/24/32)
79
+ pwBytes = []
80
+ 0.upto(nBytes-1){|i| pwBytes[i] = password.bytes.to_a[i] & 0xff || 0}
81
+ key = Aes.cipher(pwBytes, Aes.key_expansion(pwBytes)) # gives us 16-byte key
82
+ key = key.concat(key.slice(0, nBytes-16)) # expand key to 16/24/32 bytes long
83
+ # recover nonce from 1st 8 bytes of ciphertext
84
+ counterBlock = []
85
+ ctrTxt = ciphertext[0,8]
86
+ 0.upto(7){|i| counterBlock[i] = ctrTxt.bytes.to_a[i]}
87
+
88
+ #generate key Schedule
89
+ keySchedule = Aes.key_expansion(key);
90
+
91
+ # separate ciphertext into blocks (skipping past initial 8 bytes)
92
+ nBlocks = ((ciphertext.length-8)/blockSize.to_f).ceil
93
+ ct=[]
94
+ 0.upto(nBlocks-1){|b|ct[b] = ciphertext[8+b*blockSize, 16]}
95
+
96
+ ciphertext = ct; # ciphertext is now array of block-length strings
97
+
98
+ # plaintext will get generated block-by-block into array of block-length strings
99
+ plaintxt = [];
100
+ 0.upto(nBlocks-1) do |b|
101
+ 0.upto(3){|c| counterBlock[15-c] = urs(b,c*8) & 0xff}
102
+ 0.upto(3){|c| counterBlock[15-c-4] = urs((b+1)/(0x100000000-1),c*8) & 0xff}
103
+ cipherCntr = Aes.cipher(counterBlock, keySchedule) # encrypt counter block
104
+ plaintxtByte = []
105
+ 0.upto(ciphertext[b].length - 1) do |i|
106
+ # -- xor plaintxt with ciphered counter byte-by-byte --
107
+ plaintxtByte[i] = (cipherCntr[i] ^ ciphertext[b].bytes.to_a[i]).chr;
108
+ end
109
+ plaintxt[b] = plaintxtByte.join('')
110
+ end
111
+ plaintext = plaintxt.join('')
112
+ end
113
+
114
+ private
115
+
116
+ # Unsigned right shift function, since Ruby has neither >>> operator nor unsigned ints
117
+ #
118
+ # @param a number to be shifted (32-bit integer)
119
+ # @param b number of bits to shift a to the right (0..31)
120
+ # @return a right-shifted and zero-filled by b bits
121
+ def self.urs(a, b)
122
+ a &= 0xffffffff
123
+ b &= 0x1f
124
+ if a & 0x80000000 && b > 0 # if left-most bit set
125
+ a = ((a >> 1) & 0x7fffffff) # right-shift one bit & clear left-most bit
126
+ a = a >> (b - 1) # remaining right-shifts
127
+ else # otherwise
128
+ a = (a >> b); # use normal right-shift
129
+ end
130
+ a
131
+ end
132
+ end
133
+ end
@@ -1,16 +1,16 @@
1
- module Paygate
2
- class Configuration
3
- MODES = %i(live sandbox).freeze
4
-
5
- attr_reader :mode
6
-
7
- def initialize
8
- @mode = :live
9
- end
10
-
11
- def mode=(value)
12
- fail 'Invalid mode. Value must be one of the following: :live, :sandbox' unless value && MODES.include?(value.to_sym)
13
- @mode = value.to_sym
14
- end
15
- end
16
- end
1
+ module Paygate
2
+ class Configuration
3
+ MODES = %i(live sandbox).freeze
4
+
5
+ attr_reader :mode
6
+
7
+ def initialize
8
+ @mode = :live
9
+ end
10
+
11
+ def mode=(value)
12
+ fail 'Invalid mode. Value must be one of the following: :live, :sandbox' unless value && MODES.include?(value.to_sym)
13
+ @mode = value.to_sym
14
+ end
15
+ end
16
+ end