A project I work on uses the crypto module publicEncrypt function to encrypt some 32 bytes long session keys.
Due to a compatibility issue with the node-forge library we realized that, about once in a hundered iterations, the output of the publicEncrypt is 255 bytes long instead of 256.
We verified this behavior in Chrome, Firefox, and Opera, however we could not replicate the issue in a unit test (using Jest), because in this setting the output of publicEncrypt is consistently 256 bytes long.
Is this an expected behavior of the crypto module? By checking the RFC https://www.rfc-editor.org/rfc/rfc8017#page-28 this seems to be a bug to me.
Below the public key used in our tests and the unit test to illustrate the issue. To clarify, the unit test succeeeds in Jest, but calls to function publicCipher done within a browser sometimes create a 255 bytes long encrypted output.
Key Algorithm: RSA
Key Parameters: 05 00
Key Size: 2048
Key SHA1 Fingerprint: 89 BD 3D 96 DA 7E 65 60 DE B5 C2 33 5D 22 89 95 F0 44 90 EB
Public Key: 30 82 01 0A 02 82 01 01 00 AA 20 54 0F A4 1C 2A B3 BA FA C2 FB B3 C3 17 53 A0 3F C8 AB 7F 22 E0 60 E6 0A AD 80 0D EC 07 C7 FD E3 4A CD 5C 77 BC 0E FE 63 9F 6D 40 6B 21 84 20 2E F9 C4 6D 93 01 E4 CB EB D0 A5 C2 F5 F5 4F C4 F2 9E D8 C7 F5 2A C4 F2 D8 2D 87 A9 18 2B 6D B3 E2 04 EA 7B A6 5D 4A 55 1A 6D C9 1A E1 AF 4B 0C E1 FD 43 74 37 5B 1A 70 11 86 E7 E4 40 DD 6B 27 7C 72 C5 D0 15 79 B0 6D BC 07 F1 43 C7 BF D3 13 EC 94 83 D3 77 44 7B 58 2E 37 82 22 AC 07 7E 61 5F 7D A7 57 09 6C DB 24 24 5D 10 ED 83 DC AE 35 CE 6C 1E AB C2 9E 3A 1D 99 01 F9 0E 2C 6B A4 74 E0 87 1E 86 04 BC 72 A3 3E CF 46 76 57 AC 78 EC 41 AD 2D 82 E5 F5 A8 76 9D 25 0A 87 D8 D4 74 FA 64 9D 14 BF F4 17 B5 CF 2E D7 FF 71 41 94 5D 30 56 F0 17 9C ED 2F CB 66 D3 E5 7F 99 F9 31 42 EE DE 16 AE ED 38 BD E6 9A DD A6 50 75 30 5B D5 02 03 01 00 01
import { publicEncrypt, randomBytes } from 'crypto'
import constants from 'constants'
describe('Encryption output size test.', () => {
const publicKeyPem = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqiBUD6QcKrO6+sL7s8MX
U6A/yKt/IuBg5gqtgA3sB8f940rNXHe8Dv5jn21AayGEIC75xG2TAeTL69ClwvX1
T8TyntjH9SrE8tgth6kYK22z4gTqe6ZdSlUabcka4a9LDOH9Q3Q3WxpwEYbn5EDd
ayd8csXQFXmwbbwH8UPHv9MT7JSD03dEe1guN4IirAd+YV99p1cJbNskJF0Q7YPc
rjXObB6rwp46HZkB+Q4sa6R04IcehgS8cqM+z0Z2V6x47EGtLYLl9ah2nSUKh9jU
dPpknRS/9Be1zy7X/3FBlF0wVvAXnO0vy2bT5X+Z+TFC7t4Wru04veaa3aZQdTBb
1QIDAQAB
-----END PUBLIC KEY-----
`
it('Should create encrypted keys of 256 bytes.', (done) => {
for (let i = 0; i < 1000; i++) {
expect(publicCipher(publicKeyPem, randomBytes(32)).length).toEqual(256)
}
done()
})
})
function publicCipher(publicKeyPem: string, clearText: Buffer): Buffer {
return publicEncrypt({ key: publicKeyPem, padding: constants.RSA_PKCS1_PADDING }, clearText)
}
Update: the issue is due to the removal of leftmost 0 bytes. I will try find a way to make this issue reproducible, meanwhile I think the following unit test should clarify what is happening (the key pair is self signed and used for testing purposes only, so I have no problem publishing the private key):
import forge from 'node-forge'
import { privateDecrypt } from 'crypto'
import * as constants from 'constants'
it('buggy encrypted random bits from crypto', (done) => {
const encryptedRandomBits = 'inYgmlfL4y0ab5tplmDKCeP0p7q2lcnMqUDjfQpUjqFJkiqoVXQ9A8pA8hyWBf2onea/+iVMtU/Xj9IyLvPQiLkUE9OWptzmwTqnXzzG3ulXUIjud+rpvJdENSJzsX7IwiNaAPWo3EmM57B/fdXaPPGZME203SDPRVWst2XZw7zBfeM2XmS4nSMvs7NM7rGR/Ti3Hb+p7D+PJZUm7oi3npFZHSS1C/SsAXUaAja3HPaXuve+VZfclqrSbQ2P+45cRyQpvcf+ueXqdk/Yqg/sOuufR8eFDyDgRudzMOzLuH6rzrNKYtMXpD9ueFWFRh0G2wOlXNc/acbv0nSN5Dvu'
const publicKeyPem = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqiBUD6QcKrO6+sL7s8MX
U6A/yKt/IuBg5gqtgA3sB8f940rNXHe8Dv5jn21AayGEIC75xG2TAeTL69ClwvX1
T8TyntjH9SrE8tgth6kYK22z4gTqe6ZdSlUabcka4a9LDOH9Q3Q3WxpwEYbn5EDd
ayd8csXQFXmwbbwH8UPHv9MT7JSD03dEe1guN4IirAd+YV99p1cJbNskJF0Q7YPc
rjXObB6rwp46HZkB+Q4sa6R04IcehgS8cqM+z0Z2V6x47EGtLYLl9ah2nSUKh9jU
dPpknRS/9Be1zy7X/3FBlF0wVvAXnO0vy2bT5X+Z+TFC7t4Wru04veaa3aZQdTBb
1QIDAQAB
-----END PUBLIC KEY-----
`
const privateKeyPem = `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAqiBUD6QcKrO6+sL7s8MXU6A/yKt/IuBg5gqtgA3sB8f940rN
XHe8Dv5jn21AayGEIC75xG2TAeTL69ClwvX1T8TyntjH9SrE8tgth6kYK22z4gTq
e6ZdSlUabcka4a9LDOH9Q3Q3WxpwEYbn5EDdayd8csXQFXmwbbwH8UPHv9MT7JSD
03dEe1guN4IirAd+YV99p1cJbNskJF0Q7YPcrjXObB6rwp46HZkB+Q4sa6R04Ice
hgS8cqM+z0Z2V6x47EGtLYLl9ah2nSUKh9jUdPpknRS/9Be1zy7X/3FBlF0wVvAX
nO0vy2bT5X+Z+TFC7t4Wru04veaa3aZQdTBb1QIDAQABAoIBAAEK0KkF3txOHJOj
tSoNRGvRPALNNiqvCDjwkM6Lh0om6gHF+Asceqz21wJnh3EAlBAkaYhQCYoF7k/k
B9fxbM7PzJK3jkYMvIcmVuURXqIJeoQkdpFKXCDz84T/qef/DG0oR0ZuBFbNCym4
U1xdH14kMEl9t8Ah7jhF4iCSwzsEG/YUU2czCkHwnFaALsnTk6sXhcUvy8BFelIP
TU3QWxMIv2hmEJcn8GXXtORKtirpvU7WlvGSOFwKbd5yxM0oZh5ZneR2oXL/4lre
fP+jQAuAAftfMzvo4EjR/6DaHpg+YWMJMM0sncu6FwFLo0V1l8gdaiPiWRZXkTVq
aujZg9ECgYEA5Xkw2rBMWyLYVm+DgjZ2mhbRop2lWrjGKh8CYFuHXP1C1TRvUsGA
zpNyF4Gz9OAPsUm7JoG3dChr2+/or2UGrhD2BuSDPz9XIhQkQ+SOuBKTV3EcZVDz
BKajm7M+EkdVQdD8Rv5gKTHlTNtGSK+WxmSbjobCVZtjlmWDUm7DwykCgYEAvcrg
pKh20H3iLSZY+6GlngvyXe7fT6gj5t5hylHOBkwAZnlgSoRxjNWkYBsm6lCpWGvS
zYNmgxo2KyjsH59P25gyTzNOIgGpabvK+3qkiO/rDErHMM3K+/PCvYI9Ggc6idWa
p0LJniG1WD+xJTsGaFKpqXDczGDmIiEFxhBa9M0CgYAOIbyEez74jdjM2ek7Z6c+
LhGS9ipsv9xUU7yNupVHgMFQ5/3DFu+byTDJic9PUU0mGehQosRtft/Fl6y4wHv8
1EaSfhgnGPuAucTR+Y1ggKRaSjj5dFbC/pAs0okMDyCNARXIOXoqj0wTw508dLPQ
W/nIeTwWkY88f9vqg6/CoQKBgG/NKRQWIRekcC4EnvpsOLGne/iVlHrKI4wGiDi0
g9b1wm6bJXwAFRvPZmsu03fIWFm7+owR0bt9H1fBXYcrQ9GqEEjTgj1KGVAtzd7i
WjZIjn/JNRmswNw2tgsIw5GnnHZBnD7Xewlp9fesXV//K0EdINCtqYPDuQ11wn7w
d4QpAoGBAJoESzi2a+kQhIoqF+sdCWRXmtieUBvQMFqoIrIYuWMHmbUBewTByRzJ
2LAet1kkPQEScWX3OifuzScuQ2E+UB6q4JFqie+VjFItJU5OE4/45nf6IYyVBxwj
TcZR1bO+Rx1eFnj0Xyhq4Zy9iuGzJhZxpzD7P+LJG9t3rlAkOIyL
-----END RSA PRIVATE KEY-----`
const decryptedWithCrypto = privateDecrypt (
{ key: privateKeyPem, padding: constants.RSA_PKCS1_PADDING },
Buffer.from(encryptedRandomBits, 'base64')
)
const nodePrivateKey = forge.pki.privateKeyFromPem(privateKeyPem) as forge.pki.rsa.PrivateKey
const padding = Buffer.alloc(1)
padding.writeUInt8(0,0)
const padded = Buffer.concat( [ padding, Buffer.from(encryptedRandomBits, 'base64') ])
//node-forge cannot decrypt without the padding!
const decryptedFromPaddedWithNodeForge: string = nodePrivateKey.decrypt(forge.util.decode64(padded.toString('base64')))
const decryptedFromPaddedWithCrypto = privateDecrypt (
{ key: privateKeyPem, padding: constants.RSA_PKCS1_PADDING }, padded
)
expect(btoa(decryptedFromPaddedWithNodeForge)).toEqual(decryptedWithCrypto.toString('base64'))
expect(btoa(decryptedFromPaddedWithNodeForge)).toEqual(decryptedFromPaddedWithCrypto.toString('base64'))
done()
})