2

I have been trying without success to decrypt payload, a JSON web token that should contain personal information as the user data part of OAuth2 flow. According to what I have read of JWT it contains three parts: header, payload and signature - base64 encoded strings separated by a dot.

The JWT in question is this:

eyJjdHkiOiJKV1QiLCJhbGciOiJSU0EtT0FFUCIsImtpZCI6IkJGTDBzcVpEZEtPdnBEX29YSllPNDhlbEhQaldTWVB1WmZSczBtT3VLMUEiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.GFxv1iIUdktUO9f-pf_eS_J34QnBHJdSJPokE3p5XE1nPCRwIYmf3n4kq4X9T7tOAvOIEs7-BoeFedzjRQnXKetrLY5wWHCALt4C_Y8Ibu--WseiQJnIKzyEvuhZHonRN6GWaMZB724M2NhtHHXY4pLX7RNsXquaMBFG1sFny20zHWEjSGx4jDOfKkH-f9Ty5hRIKvZXmQ36RsJI4oF1i4j1aPZj-BKiyiorm5c5F-IUcukhm2QObf88K3EFS0mbVvPM7yNAure51LWptBLIKBBq7VPMfq0NXLL_bBzMZ1XdFBDt2qkIteoE9ts44esTeVvsvNSRHGqAtl8EO-10wg.jOvZCvp2DULxkMyiybRjXw.CCeHLVU2GjsSrdND6UYxIN9Ny_QKig7CC_E3uDsHsb1mRaDV9gQ432NGYpD0RndPQoepOppcDsI2zT2yoKCYLYKuig6XDcOl3eK4-MooDjZOHn_9ahYlAiauTFRtiJfHU9rTSMU_eLkH-z1eyzoe3fbEqkK6hWxHELzlYByeof0pINcDRsxwcNG0GDAqrcJ3IdQjLwoupVGLZh-wb3WsqpZbV5Abz2LkV3BK_TqVjlEBEtKBXOZohdieIwEo1kUm_MZJpXzF6HIJkasrrN1c3-uVmMMJbRBDfZv0GBZ4OyNv9q5vTKCY90RF3QVmN604L9v2JAalOmhLgRq1q8on_dk35nKuhynSrr9lMy2ieTD5VcpbDODpwGHPBQJnn99dbYZqu3uWKYURR2mmfCq1IDEgtq3aczgmNmU6QKQ8vUgljSQ75cqwS9GaHHPHuJZeMXRa1ifkuywf7z5kgk8Z4z_jKA1fqnlYSmRASponkuHqkVx-P5ZlRqfjvR9qpG2qz3B4a1Cjmnp2Di8_5jTg09CLYKBMfa42Fgw5lnlivEcGvWIWWd4a0FvYQlDCjDkRfv21DFkleJ23CyJ7Zjsm2CTPZXk9y6G3HzOYgFxyriIw_52VpnzIrgmYsmpm5amBKGbYYeZbthPwVnf1W76yqBziYljAoIP7g1uM0HdjvkqeDTe3Q5_WQAdrmIz8Yjh2z0edjc0ynPPqKLMfEs8SQWMYsodgQ6KcVMGWalYPCV6eF79Ldf5gpah_oXTMP0nn7rKccoiftlNFsgem2p81yTN7AwWKm_fO70EFdpQ1-kez7lQ5OtamdZWR-W3sEtn3K0wdeLbMYtD4bD8RdwKoZ_DZgBLdJenmXAa7P5dkOzxaeUAKTAaYOSq_EKxIH8AtnqyTTXEfRXLxyoFGJlN_N6bSUWytbJr4c1ktxBG4RKWWJpusVA2hiZ2GCFs7PfWRPC7hSFd8V52O1hwuZsAz6BN4pJv1rIicVSJNozMgyv9ZqY5HkOTIUUztsu_Urkre1ItfVcAg1Ia5Fj-OZ7ymm7sNA9JKBV14T4EPtfAcnoLLRr95ngTzsBPu7KKb-EO7k7m1na_sisQaQxlQNJset5_Hi_Oj2Sj1g7tB7Uld64MlQbvOBxBIw_D9hmmjm_v1cbDcLV2XmmXQotLout197vcJahNQOvSb7W9BqXFvxVlSHa89_H75acO4IMYem3lRJXT2ZdwsQhvFZ8BRSN7h-kKl6Z_v3aRNTrbwA_qQDYlH67uP84F9FKqejejCgNGtqzbvNs4SYv64Wsq9Uz6xhAeCXBuxn9EQc83cKsNkUnLwREHTlng3CpQHpZje6dAEpKKOIN5XHOcj04BNwAZP0TnCeGUWqZpxPI3vyFupC5vb5Wt26BOC1MWb0Upn08xAxwY5urtkX4cRFMpnXFHnv-ypfr2UY5KP6Nh2tbahEgZ-1bBw3GaEPYAqros31pyY6E51nCQ2j3kdmFxVuW3fTp5CzHpAPuTSi6zX6r8sttUi-bJK81CTey8TMFtr5sqpYtlLHbUpSsi-YfYeNCfOmSMLESMQRDSh7LmBFbGK9zDavuTALTNMNPVWeJUQOeAcScQWFZLEg8-attDjdcpxVEZAJIXb9frYD4aDyE4WE0vsRj-kP3d0txJ1_c-mG-PjW0_IVIo_9bUHteHGZdyMS1y6NNOhoTlq0XXAs0oRcd7RVojwUYpbVGAvWT7_CDPTKvnJDFALbAu2D7KWFw1dCnDrUW5HzxW274Y0wx25ev3BKpVE5tekBujVNVmqNjIh._JyU-CpPzHItUbTb_syKsg

The payload comes from a service that is used by the Finnish government, so I would guess that its format should be correct, but first I am confused that the JWT contains five parts. First part is headers:

{"cty":"JWT","alg":"RSA-OAEP","kid":"BFL0sqZDdKOvpD_oXJYO48elHPjWSYPuZfRs0mOuK1A","enc":"A128CBC-HS256"}

Payload should be encrypted with this public/private key:

const jsonKey = {
  p: '7C8pWx6iaZDypKgElaJoDGN_OdZh-mAI6jAZkoA9Io5vQrTNzqdFyTF1j_AsU4KMKATvihtKoTAnZnfH1Dk-nCX6GLpiDOeZAXyv-tTGfINl50YhbI-qu4B8h-CPohzOXpBMrOCplQwR60NcW827aNVu1OTBPRboWgtAyqduWGE',
  kty: 'RSA',
  q: '3Pxz70nzwiNWQwfXQEXA529HoGP6aFBFY2HRnOWF1M7OlN4OnHE-WRlUeyMFA1junBOtUbBhq8hO3X0zPNZVwUrA1IbzoBZUmCTaL4ClZ6dDATfFMrebj8455wCqJNfpkiAXLhA4eh5j1daAWqMyhr9skx5ZslSYbeRM52zmWUc',
  d: 'l6a9RMaox9dYchDoOWHZ_IpXLfZZyN2k47fljDluwkIs4wCGjBbvt9nAX2mjsNttWi7mihLcea0PO5wt9dHhzwrB9xbAKlHNh4_IK9YJhdCg47rECtdDhIHqnvTHx4zYWViC6MBorR094pOQy57Hg2kkJhVkeTLeruBmL1QFf7AkoIICJ11FCIwYHJW8bRfWTBbqh8Rnwq2z620ZE16ZsHTp1TfXWWyOXIuMZXAZfdiQq95zR6npkEdGs_es8mwqgXroXmE2UQMEz-r-9DpTXk3_cbXLFbo8PhrpfD8Mxe8hQOdkD5yGqzGz4Xgnl62iCTRZOwdxH3ayH6z9vXDTAQ',
  e: 'AQAB',
  use: 'enc',
  kid: 'BFL0sqZDdKOvpD_oXJYO48elHPjWSYPuZfRs0mOuK1A',
  qi: 'XCkgo29q4ha8iwGc9VJq9y2TsAK7WxjcTZlcOoYFkJqzAdxlBvYKBJ4H_2OQvURo5sVhwWQbHrd_nfHVKe8vos9AYz3b0ylQJODse8Xm2w3gaTrwdb4Z5EN2I_2513O60XIeg2WSLMSkvkfaSkJ20OCM7OeZwVyE_N8dTyafy84',
  dp: 'dw1y0D6J6aKp3Lvgy7h4sD37JKFe9AEynTGvwjwoFOItTTesQk3pDHiE5RBQl6vHkGikgj4tiUCnq6wXK2_LkpRGE-7ne8_GPYynfE2C28K0PDcKpBlrG2ax3yAf5ryUffBI5h-8-6eA5NEonhH_NOHZIzFIAs4oQzNG7qAlLCE',
  alg: 'RS256',
  dq: 'WpjVHuD-ojTChLCOOrdeIoOopcTXQDTIfbn4qY4fk-NFJhrzeoeiu_x6ehEdWQX4rNwUTk01faudSYiunN5yQdBXxcmdz5_YBpf1K0xeg2Q7QCCRI_3KNOajLmVDW994zoOBfU0BGm-jFmPeM0p8yGlqJdZnh4jPBR53uNkYhrk',
  n: 'y-Fw7PfbnBie2s408nZICscdQ0-spOTeli0LIkFi5QMgGnQ2mQlTXcypm8UdkYu-Up4tzFUmkd-n0gPKES6cP1YGWn9gXC9DP1GmHQVXuGGAxMLkctYK2-CSCckgLtNcmxzdEtwbtwOmWHFdtBUFvwLKnqU0XEv_wdFEakKHwp12foSUoUH7FbzMbLu5BHo4rjuTU2paHQpiHqoG9qNg-jZFXLdjRGWslJKCJGDUBXB3HIlGykr8ghOdsMlpzRn3zQ8WI77bPN58QlGMXVOQcKivsKhoF9mZOHEceAfPLKgucmI-KwJ7w-6DTAuwCI6eo6hlQc_Gz0JxjPJdH3o75w'
}

Currently I am only able to decrypt the payload with this library:

const { Crypto } = require('@peculiar/webcrypto')
const crypto = new Crypto()

// Payload defined above
const parts = payload.split('.')

function ab2str (buf) {
  return String.fromCharCode.apply(null, new Uint8Array(buf))
}

const decrypt = async () => {
  try {
    const k = await crypto.subtle.importKey('jwk', jsonKey, { name: 'RSA-OAEP', hash: 'SHA-1' }, false, ['decrypt'])
    const data = await crypto.subtle.decrypt('RSA-OAEP', k, Buffer.from(`${parts[1]}`, 'base64'))
    return ab2str(data)
  } catch (err) {
    console.log(err)
  }
}

decrypt()

Decrypting yields ñDõ^îó"ìût=©Z]¬úí³ê which is not a JSON object. Other parts (or combination of them) will throw an error from the library. Changing anything in the payload or key will throw exceptions, so I guess that the phase should be correct, but the output is not.

My question is: how can I decrypt this payload using node.js or is it actually broken?

P.S. Private keys and JWT are using testing environment, so they don't contain any real secrets.

adrenalin
  • 1,656
  • 1
  • 15
  • 25
  • 1
    No, it’s automated testing data. There is nothing sensitive here, but thank you for asking! – adrenalin Nov 12 '20 at 19:08
  • If decryption doesn't fail then the answer is likely correct, because OAEP will fail to decrypt if you have the wrong private key or ciphertext. Could you print out the data you get in hexadecimals rather than using `ab2str`? What you are probably getting is the direct output of OAEP decryption, which is likely a 128 or 256 bit AES data key (hybrid cryptography). This can then be used to decrypt the next part. – Maarten Bodewes Nov 12 '20 at 19:23
  • Uint8Contents: ``, byteLength 32 – adrenalin Nov 12 '20 at 19:30
  • 1
    This is what I can find from the documentation: 'id_token is returned, since the requested scope contains value "openid". Id_token is a JWT encrypted with the client’s public key and signed with Telia’s private key. The client must decrypt the ID token with its private key, and the client may verify Telia’s signature with Telia’s public key.' 'In case signing and encryption are used, payload signature must be verified and its data decrypted. Note: encryption must be enabled before production launch to comply with Traficom regulation 72.' – adrenalin Nov 12 '20 at 19:38
  • 1
    *but first I am confused that the JWT contains five parts* - a normal, signed JWT (aka. JWS) contains 3 parts, but you got a [JWE](https://medium.facilelogin.com/jwt-jws-and-jwe-for-not-so-dummies-b63310d201a3), an encrypted JWT – jps Nov 12 '20 at 19:57
  • 1
    Note to other readers, the value in the `Uint8Contents` above is not correct, it's for a different JWE. – Maarten Bodewes Nov 13 '20 at 08:21

2 Answers2

2

The components can be found by splitting them on the dot, with the components being base 64 URL encoded.

The header is at the start or component #0, it shows that this is hybrid encryption, with AES-CBC and HMAC used for authenticated encryption.

Your RSA-OAEP decryption of component #1 succeeds. OAEP will error if the ciphertext and key don't match. However, it doesn't return the plaintext, it returns a 16 byte HMAC key and a 16 byte AES key (in that order!). The key values are (in hex):

19D7657D5CDBF41E48EB4FF35A970406 DC803B73BD16AEC1B2DF85FCBBD106F3

Note that the keys are dependent on the authenticated encryption algorithm set in the header. You may want to test that header first to see if an acceptable algorithm is used (not the other way around, as that would enable a downgrade attack on the algorithm, never rely on input for security parameters).

By the way, the OAEP decryption uses MGF1 with SHA-1 internally, the default; don't worry about the SHA-1 in there, the hash doesn't have much influence over the security of OAEP.

The IV can be found in the 3rd component, component #2:

8CEBD90AFA760D42F190CCA2C9B4635F

You can use AES-128 / ECB / PKCS#7 padding to decrypt component #3.

Finally, to match the authentication tag in component #4, you need to calculate the HMAC-SHA-256 of:

A | IV | ciphertext | AL

Where A is the additional authenticated data. This is the ASCII encoding of the base 64 url encoded header (!). Finally, the AL is the 64 bit big integer representation of the length of A in bits (i.e. the number of bytes times eight).

So in total that should be:

65794A6A64486B694F694A4B563151694C434A68624763694F694A53553045745430464655434973496D74705A434936496B4A475444427A635670455A457450646E424558323959536C6C504E44686C62456851616C645457564231576D5A53637A42745433564C4D5545694C434A6C626D4D694F694A424D54493451304A444C5568544D6A5532496E30
8CEBD90AFA760D42F190CCA2C9B4635F
0827872D55361A3B12ADD343E9463120DF4DCBF40A8A0EC20BF137B83B07B1BD6645A0D5F60438DF63466290F446774F4287A93A9A5C0EC236CD3DB2A0A0982D82AE8A0E970DC3A5DDE2B8F8CA280E364E1E7FFD6A16250226AE4C546D8897C753DAD348C53F78B907FB3D5ECB3A1EDDF6C4AA42BA856C4710BCE5601C9EA1FD2920D70346CC7070D1B418302AADC27721D4232F0A2EA5518B661FB06F75ACAA965B57901BCF62E457704AFD3A958E510112D2815CE66885D89E230128D64526FCC649A57CC5E8720991AB2BACDD5CDFEB9598C3096D10437D9BF41816783B236FF6AE6F4CA098F74445DD056637AD382FDBF62406A53A684B811AB5ABCA27FDD937E672AE8729D2AEBF65332DA27930F955CA5B0CE0E9C061CF0502679FDF5D6D866ABB7B962985114769A67C2AB5203120B6ADDA73382636653A40A43CBD48258D243BE5CAB04BD19A1C73C7B8965E31745AD627E4BB2C1FEF3E64824F19E33FE3280D5FAA79584A64404A9A2792E1EA915C7E3F966546A7E3BD1F6AA46DAACF70786B50A39A7A760E2F3FE634E0D3D08B60A04C7DAE36160C39967962BC4706BD621659DE1AD05BD84250C28C39117EFDB50C5925789DB70B227B663B26D824CF65793DCBA1B71F3398805C72AE2230FF9D95A67CC8AE0998B26A66E5A9812866D861E65BB613F05677F55BBEB2A81CE26258C0A083FB835B8CD07763BE4A9E0D37B7439FD640076B988CFC623876CF479D8DCD329CF3EA28B31F12CF12416318B2876043A29C54C1966A560F095E9E17BF4B75FE60A5A87FA174CC3F49E7EEB29C72889FB65345B207A6DA9F35C9337B03058A9BF7CEEF4105769435FA47B3EE54393AD6A6759591F96DEC12D9F72B4C1D78B6CC62D0F86C3F117702A867F0D98012DD25E9E65C06BB3F97643B3C5A79400A4C0698392ABF10AC481FC02D9EAC934D711F4572F1CA814626537F37A6D2516CAD6C9AF873592DC411B844A596269BAC540DA1899D86085B3B3DF5913C2EE148577C579D8ED61C2E66C033E81378A49BF5AC889C55224DA33320CAFF59A98E4790E4C8514CEDB2EFD4AE4ADED48B5F55C020D486B9163F8E67BCA69BBB0D03D24A055D784F810FB5F01C9E82CB46BF799E04F3B013EEECA29BF843BB93B9B59DAFEC8AC41A431950349B1EB79FC78BF3A3D928F583BB41ED495DEB832541BBCE071048C3F0FD8669A39BFBF571B0DC2D5D979A65D0A2D2E8BADD7DEEF7096A13503AF49BED6F41A9716FC559521DAF3DFC7EF969C3B820C61E9B79512574F665DC2C421BC567C05148DEE1FA42A5E99FEFDDA44D4EB6F003FA900D8947EBBB8FF3817D14AA9E8DE8C280D1ADAB36EF36CE1262FEB85ACABD533EB18407825C1BB19FD11073CDDC2AC3645272F04441D39678370A9407A598DEE9D004A4A28E20DE571CE723D3804DC0064FD139C2786516A99A713C8DEFC85BA90B9BDBE56B76E81382D4C59BD14A67D3CC40C70639BABB645F871114CA675C51E7BFECA97EBD9463928FE8D876B5B6A112067ED5B070DC66843D802AAE8B37D69C98E84E759C24368F791D985C55B96DDF4E9E42CC7A403EE4D28BACD7EABF2CB6D522F9B24AF350937B2F13305B6BE6CAA962D94B1DB5294AC8BE61F61E3427CE99230B1123104434A1ECB98115B18AF730DABEE4C02D334C34F55678951039E01C49C4161592C483CF9AB6D0E375CA715446402485DBF5FAD80F8683C84E16134BEC463FA43F7774B71275FDCFA61BE3E35B4FC8548A3FF5B507B5E1C665DC8C4B5CBA34D3A1A1396AD175C0B34A1171DED15688F0518A5B54602F593EFF0833D32AF9C90C500B6C0BB60FB296170D5D0A70EB516E47CF15B6EF8634C31DB97AFDC12A9544E6D7A406E8D53559AA363221
0000000000000458

Note that authentication tags are best tested using a time-constant compare.

And then you get the JWT token of Väinö, definitely a test set fortunately.

Good luck!

Maarten Bodewes
  • 90,524
  • 13
  • 150
  • 263
2

In the end the solution was very simple. I had tried to use the whole JSON key for a couple of different libraries, but using just the PEM version without names allowed to use jose, while key store failed to find it even though JWE headers clearly described the key used for encryption.

const { JWK, JWE } = require('jose')
const privateKey = JWK.asKey(/* private key here as PEM */)
const jwe = /* encrypted JWE here */
const jwt = JWE.decrypt(jwe, privateKey)
const payload = Buffer.from(jwt.toString().split('.')[1], 'base64')
const data = JSON.parse(payload)
console.log(data)
adrenalin
  • 1,656
  • 1
  • 15
  • 25