Panasonic TV - Pincode and encryption support

Just wanted to create a dedicated post for something I’m going to be playing with over the come days/weeks/months and that’s to be able to control my new Panasonic TV via Vera. Thankfully so much of the hard work has already been done and some great posts have been made elsewhere so I’m just going to bring them all together, and who knows maybe there is someone else out there interested and happy to help…

This was the post that first made me aware of the PIN and encryption requirements

And then more recently I came across this that actually has pretty much all the Lua code done already, the challenge is going to be making it work via Vera.

https://forum.logicmachine.net/showthread.php?tid=232

First point of call, libraries/modules and requesting the TV to present me with a pin code to use for the pairing process.

Step 1 - To get a pin code to display on the tv for the pairing the then occur.

encdec = require('encdec')
aes = require('user.aes')
require('json')

ip = '192.168.102.200'

name = 'My Remote'
URL_CONTROL_NRC = 'nrc/control_0'
URL_CONTROL_DMR = 'dmr/control_0'
URL_CONTROL_NRC_DEF = 'nrc/sdd_0.xml'

URN_RENDERING_CONTROL = 'schemas-upnp-org:service:RenderingControl:1'
URN_REMOTE_CONTROL = 'panasonic-com:service:p00NetworkControl:1'

function request(host, url, urn, action, args)
  local body, http, ltn12, sink, res, err

  ltn12 = require('ltn12')
  http = require('socket.http')
  body = [[<?xml version="1.0" encoding="utf-8"?>
  <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <s:Body><u:]] .. action .. [[ xmlns:u="urn:]] .. urn .. [[">]] .. args .. [[</u:]] .. action .. [[></s:Body>
  </s:Envelope>]]

  sink = {}

  res, err = http.request({
    url = 'http://' .. host .. ':55000/' .. url,
    method = 'POST',
    headers = {
      ['soapaction'] = '"urn:' .. urn .. '#' .. action .. '"',
      ['Content-Type'] = 'text/xml; charset=utf-8', 
      ['Content-Length'] = #body,
    },
    sink = ltn12.sink.table(sink),
    source = ltn12.source.string(body),
  })

  if sink then
   -- log(table.concat(sink))
    
    return table.concat(sink)
  else
    return nil, err
  end
end


function request_pin_code()
  pairing_res = request(ip, URL_CONTROL_NRC, URN_REMOTE_CONTROL, 'X_DisplayPinCode', '<X_DeviceName>' .. name .. '</X_DeviceName>')
  if pairing_res then
  challenge_Key = string.match(pairing_res,'<X_ChallengeKey>(.*)</X_ChallengeKey>')
   iv = encdec.base64dec(challenge_Key) 
    storage.set('IV', iv)
  end
end

My first challenge looks to be the base64 encoding/decoding part.

The code above uses ‘ iv = encdec.base64dec(challenge_Key) ’ and while thanks to @rigpapa who made me aware that the mime library has a base64 encode/decode option, i’m going to use/customise a base64 function I found online to do the decode, in order to help me learn/troubleshoot better and hopefully preserve the original code a bit more.

I just changed ‘.’ to ‘_’ = ‘ iv = encdec_base64dec(challenge_Key) ’

-- decoding
function encdec_base64dec(data)
    local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
    data = string.gsub(data, '[^'..b..'=]', '')
    return (data:gsub('.', function(x)
        if (x == '=') then return '' end
        local r,f='',(b:find(x)-1)
        for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end
        return r;
    end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
        if (#x ~= 8) then return '' end
        local c=0
        for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end
        return string.char(c)
    end))
end

After doing that you can call the ‘request_pin_code()’ function and success the TV prompts me with a PIN code for pairing !!! :partying_face:

Here is the updated code for to get the PIN Pairing prompt.

local ip = '192.168.102.200'
local name = 'My Vera Remote'
local URL_CONTROL_NRC = 'nrc/control_0'
local URL_CONTROL_DMR = 'dmr/control_0'
local URL_CONTROL_NRC_DEF = 'nrc/sdd_0.xml'

local URN_RENDERING_CONTROL = 'schemas-upnp-org:service:RenderingControl:1'
local URN_REMOTE_CONTROL = 'panasonic-com:service:p00NetworkControl:1'

local function encdec_base64dec(data)
    local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
    local data = string.gsub(data, '[^'..b..'=]', '')
    return (data:gsub('.', function(x)
        if (x == '=') then return '' end
        local r,f='',(b:find(x)-1)
        for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end
        return r;
    end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
        if (#x ~= 8) then return '' end
        local c=0
        for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end
        return string.char(c)
    end))
end

function request(host, url, urn, action, args)
  local body, http, ltn12, sink, res, err

  local ltn12 = require('ltn12')
  local http = require('socket.http')
  local body = [[<?xml version="1.0" encoding="utf-8"?>
  <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <s:Body><u:]] .. action .. [[ xmlns:u="urn:]] .. urn .. [[">]] .. args .. [[</u:]] .. action .. [[></s:Body>
  </s:Envelope>]]

  sink = {}

  res, err = http.request({
    url = 'http://' .. host .. ':55000/' .. url,
    method = 'POST',
    headers = {
      ['soapaction'] = '"urn:' .. urn .. '#' .. action .. '"',
      ['Content-Type'] = 'text/xml; charset=utf-8', 
      ['Content-Length'] = #body,
    },
    sink = ltn12.sink.table(sink),
    source = ltn12.source.string(body),
  })

  if sink then
   -- log(table.concat(sink))
    
    return table.concat(sink)
  else
    return nil, err
  end
end


function request_pin_code()
  local pairing_res = request(ip, URL_CONTROL_NRC, URN_REMOTE_CONTROL, 'X_DisplayPinCode', '<X_DeviceName>' .. name .. '</X_DeviceName>')
  if pairing_res then
  challenge_Key = string.match(pairing_res,'<X_ChallengeKey>(.*)</X_ChallengeKey>')
   iv = encdec_base64dec(challenge_Key) 
    storage.set('IV', iv)
  end
end

request_pin_code()

The next step, is going to be a bit more complex as this is where the pairing occurs, and not being an expert in encryption/keys/pairing etc. I’m flying a bit blind with this…

Step 2 = with the pin displayed on the tv then call function authorise_pin_code(‘9712’) with the pin from the tv, that completes the pairing process, the tv will show message “pairing process complete”

My challenge here is to find comparable libraries/modules, as this part seems way more complex that the pin request part, as it has calls like these that I need to map/replace …

I would really appreciate any guidance on alternatives for other calls such as those below e.g hex to/from string and aes_cbc encrypt/decrypt etc.

lmcore.strtohex(decrypted) 
lmcore.hextostr('15C95AC2B08AA7EB4E228F811E34D04FA54BA7DCAC9879FA8ACDA3FC244F3854', true)
aes_cbc:decrypt(encdec.base64dec(data))
aes_cbc:encrypt(payload)
encdec.hmacsha256(ciphertext, hmac_key, true)
encdec.base64enc(ciphertext .. sig)

Functions I’ve extracted based on the pairing process…


function request_session_id(app_id, sesh_key, sesh_hmac_key, sesh_iv)
  encinfo = encrypt_soap_payload('<X_ApplicationId>' .. app_id .. '</X_ApplicationId>',sesh_key, sesh_hmac_key, sesh_iv)
  params = ('<X_ApplicationId>'..app_id..'</X_ApplicationId>'.. '<X_EncInfo>'..encinfo..'</X_EncInfo>' )
  sesh_id = request(ip, URL_CONTROL_NRC, URN_REMOTE_CONTROL, 'X_GetEncryptSessionId', params) 
  return sesh_id
end 

function encrypt_soap_payload(data, key, hmac_key, iv)
payload = '000000000000'
  n = #data

payload = payload .. string.char(bit.band(bit.rshift(n, 24), 0xFF))
payload = payload .. string.char(bit.band(bit.rshift(n, 16), 0xFF))
payload = payload .. string.char(bit.band(bit.rshift(n, 8), 0xFF))
payload = payload .. string.char(bit.band(n, 0xFF))

payload = payload .. data

 aes_cbc, err = aes:new(key, nil, aes.cipher(128, 'cbc'), { iv = iv }, nil, 1) 
  ciphertext = aes_cbc:encrypt(payload)
  sig = encdec.hmacsha256(ciphertext, hmac_key, true)
  encrypted_payload = encdec.base64enc(ciphertext .. sig)
  return encrypted_payload
end


function decrypt_soap_payload(data, key, hmac_key, iv)
  aes_cbc, err = aes:new(key, nil, aes.cipher(128, 'cbc'), { iv = iv }, nil, 0) 
  decrypted = aes_cbc:decrypt(encdec.base64dec(data))
  decrypted = string.gsub(string.sub(lmcore.strtohex(decrypted), 33), '%x%x', function(value) return string.char(tonumber(value, 16)) end) 
  return decrypted
end

function get_session_keys(enc_key)
  iv = encdec.base64dec(enc_key)
  iv_vals = { iv:byte(1, -1) }
  key_vals = {}

for i = 1, 16, 4 do
  key_vals[ i ] = iv_vals[ i + 2]
  key_vals[ i + 1 ] = iv_vals[ i + 3]
  key_vals[ i + 2 ] = iv_vals[ i ]
  key_vals[ i + 3 ] = iv_vals[ i+ 1]
  end
  
  sesh_key = string.char(unpack(key_vals))
  sesh_hmac_key = iv..iv
  sesh_iv = iv
  
  return sesh_key, sesh_hmac_key, sesh_iv
end


function authorise_pin_code(pincode)

  iv = storage.get('IV')
  iv_vals = { iv:byte(1, -1) }
  key_vals = {}

for i = 1, 16, 4 do
  key_vals[ i ] = bit.band(bit.bnot(iv_vals[ i + 3 ]), 0xFF)
  key_vals[ i + 1 ] = bit.band(bit.bnot(iv_vals[ i + 2 ]), 0xFF)
  key_vals[ i + 2 ] = bit.band(bit.bnot(iv_vals[ i + 1 ]), 0xFF)
  key_vals[ i + 3 ] = bit.band(bit.bnot(iv_vals[ i ]), 0xFF)
end

key = string.char(unpack(key_vals))
hmac_key_mask = lmcore.hextostr('15C95AC2B08AA7EB4E228F811E34D04FA54BA7DCAC9879FA8ACDA3FC244F3854', true)
hmac_key_mask_vals = { hmac_key_mask:byte(1, -1) }
hmac_vals = {}

for i = 1, 32, 4 do
  hmac_vals[ i ] = bit.bxor(hmac_key_mask_vals[ i ], iv_vals[ bit.band(i + 1, 0xF) + 1 ])
  hmac_vals[ i + 1 ] = bit.bxor(hmac_key_mask_vals[ i + 1 ], iv_vals[ bit.band(i + 2, 0xF) + 1 ])
  hmac_vals[ i + 2 ] = bit.bxor(hmac_key_mask_vals[ i + 2 ], iv_vals[ bit.band(i - 1, 0xF) + 1 ])
  hmac_vals[ i + 3 ] = bit.bxor(hmac_key_mask_vals[ i + 3 ], iv_vals[ bit.band(i, 0xF) + 1 ])
end

  hmac_key = string.char(unpack(hmac_vals))
    
  params = '<X_AuthInfo>' .. encrypt_soap_payload("<X_PinCode>" .. pincode .. "</X_PinCode>",  key, hmac_key, iv)  .. '</X_AuthInfo>'
  authorise_res = request(ip, URL_CONTROL_NRC, URN_REMOTE_CONTROL, 'X_RequestAuth', params)
  auth_res = string.match(authorise_res,'<X_AuthResult>(.*)</X_AuthResult>')
  
  decrypted = decrypt_soap_payload(auth_res, key, hmac_key, iv)
  app_id = string.match(decrypted,'<X_ApplicationId>(.*)</X_ApplicationId>')
  enc_key = string.match(decrypted,'<X_Keyword>(.*)</X_Keyword>')
  
  -- get AES and Hmac keys from x_keyword  
  sesh_key, sesh_hmac_key, sesh_iv = get_session_keys(enc_key)
  -- request session key to be able to send encrypted commands  
  sesh_id_res = request_session_id(app_id, sesh_key, sesh_hmac_key, sesh_iv)
  enc_result = string.match(sesh_id_res,'<X_EncResult>(.*)</X_EncResult>')
  enc_result = decrypt_soap_payload(enc_result, sesh_key, sesh_hmac_key, sesh_iv)
  
  --Set session ID and begin sequence number at 1. We have to increment the sequence number upon each successful NRC command.
  sesh_id = string.match(enc_result,'<X_SessionId>(.*)</X_SessionId>')
  sesh_seq_num = 1
  
  session_keys = json.encode({
       SESSION_ID = sesh_id,
       SESSION_SEQUENCE_NUMBER = sesh_seq_num,
       SESSION_IV = sesh_iv,
       SESSION_HMAC_KEY = sesh_hmac_key, 
       SESSION_KEY = sesh_key,
       APP_ID = app_id,
       KEY = key, 
       HMAC_KEY = hmac_key, 
       IV = iv
        }) 
  storage.set('session_keys', session_keys)
  return sesh_id, sesh_seq_num, sesh_iv
  
end


I’ve come across this on GitHub - GitHub - somesocks/lua-lockbox: A collection of cryptographic primitives written in pure Lua - which seems to cover many of the capabilities I need.

Has anyone used this before ?

While Lua-lockbox looked promising, I’ve tried to use the above code with this library (focussing on the base64 module, on a standalone Lua 5.3 set up but could not get it to work.

Will revisit it again later, for now I’ll keep trying to find other modules to use…

May have found some alternatives to the logic machine module.functions used, although I have no idea if it will return the same as the proprietary one used below…

lmcore.strtohex() 
lmcore.hextostr()

With the following…

local function strtohex(str)
   return (str:gsub(".", function(char) return string.format("%2x", char:byte()) end))
end

local function hextostr(hex)
   return (hex:gsub("%x%x", function(digits) return string.char(tonumber(digits, 16)) end))
end

As for the aes ones.

aes_cbc:decrypt()
aes_cbc:encrypt()

aes.lua seems to be available here → Logic Machine Forum although I’m not entirely sure how this will map, but I’m told it’s the same module used in the original…

And possibly for the hmac one.

encdec.hmacsha256()

here → hmac - luapower.com
hmac/hmac.lua at master · luapower/hmac · GitHub