mirror of https://gerrit.osmocom.org/pysim
ARA-M related command support
This introduces support for talking to the ARA-M application on a card, as specified in the GlobalPlatform "Secure Element Access Control" specification v1.1. Change-Id: Ia9107a4629c3d68320f32bbd4dd26e1f430717da
This commit is contained in:
parent
a4df942fe6
commit
95ce6b1708
|
@ -443,6 +443,94 @@ authenticate
|
|||
:func: ADF_USIM.AddlShellCommands.authenticate_parser
|
||||
|
||||
|
||||
ARA-M commands
|
||||
--------------
|
||||
|
||||
The ARA-M commands exist to manage the access rules stored in an ARA-M applet on the card.
|
||||
|
||||
ARA-M in the context of SIM cards is primarily used to enable Android UICC Carrier Privileges,
|
||||
please see https://source.android.com/devices/tech/config/uicc for more details on the background.
|
||||
|
||||
|
||||
aram_get_all
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Obtain and decode all access rules from the ARA-M applet on the card.
|
||||
|
||||
NOTE: if the total size of the access rules exceeds 255 bytes, this command will fail, as
|
||||
it doesn't yet implement fragmentation/reassembly on rule retrieval. YMMV
|
||||
|
||||
::
|
||||
|
||||
pySIM-shell (MF/ADF.ARA-M)> aram_get_all
|
||||
[
|
||||
{
|
||||
"ResponseAllRefArDO": [
|
||||
{
|
||||
"RefArDO": [
|
||||
{
|
||||
"RefDO": [
|
||||
{
|
||||
"AidRefDO": "ffffffffffff"
|
||||
},
|
||||
{
|
||||
"DevAppIdRefDO": "e46872f28b350b7e1f140de535c2a8d5804f0be3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ArDO": [
|
||||
{
|
||||
"ApduArDO": {
|
||||
"generic_access_rule": "always"
|
||||
}
|
||||
},
|
||||
{
|
||||
"PermArDO": {
|
||||
"permissions": "0000000000000001"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
aram_get_config
|
||||
~~~~~~~~~~~~~~~
|
||||
Perform Config handshake with ARA-M applet: Tell it our version and retrieve its version.
|
||||
|
||||
NOTE: Not supported in all ARA-M implementations.
|
||||
|
||||
.. argparse::
|
||||
:module: pySim.ara_m
|
||||
:func: ADF_ARAM.AddlShellCommands.get_config_parser
|
||||
|
||||
|
||||
aram_store_ref_ar_do
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
Store a [new] access rule on the ARA-M applet.
|
||||
|
||||
.. argparse::
|
||||
:module: pySim.ara_m
|
||||
:func: ADF_ARAM.AddlShellCommands.store_ref_ar_do_parse
|
||||
|
||||
For example, to store an Android UICC carrier privilege rule for the SHA1 hash of the certificate used to sign the CoIMS android app of Supreeth Herle (https://github.com/herlesupreeth/CoIMS_Wiki) you can use the following command:
|
||||
|
||||
::
|
||||
|
||||
pySIM-shell (MF/ADF.ARA-M)> aram_store_ref_ar_do --aid FFFFFFFFFFFF --device-app-id E46872F28B350B7E1F140DE535C2A8D5804F0BE3 --android-permissions 0000000000000001 --apdu-always
|
||||
|
||||
|
||||
aram_delete_all
|
||||
~~~~~~~~~~~~~~~
|
||||
This command will request deletion of all access rules stored within the
|
||||
ARA-M applet. Use it with caution, there is no undo. Any rules later
|
||||
intended must be manually inserted again using `aram_store_ref_ar_do`
|
||||
|
||||
|
||||
|
||||
cmd2 settable parameters
|
||||
------------------------
|
||||
|
|
|
@ -49,6 +49,7 @@ from pySim.ts_51_011 import CardProfileSIM, DF_TELECOM, DF_GSM
|
|||
from pySim.ts_102_221 import CardProfileUICC
|
||||
from pySim.ts_31_102 import CardApplicationUSIM
|
||||
from pySim.ts_31_103 import CardApplicationISIM
|
||||
from pySim.ara_m import CardApplicationARAM
|
||||
from pySim.gsm_r import DF_EIRENE
|
||||
|
||||
# we need to import this module so that the SysmocomSJA2 sub-class of
|
||||
|
@ -86,6 +87,7 @@ def init_card(sl):
|
|||
profile = CardProfileUICC()
|
||||
profile.add_application(CardApplicationUSIM())
|
||||
profile.add_application(CardApplicationISIM())
|
||||
profile.add_application(CardApplicationARAM())
|
||||
rs = RuntimeState(card, profile)
|
||||
|
||||
# FIXME: do this dynamically
|
||||
|
|
|
@ -0,0 +1,364 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# without this, pylint will fail when inner classes are used
|
||||
# within the 'nested' kwarg of our TlvMeta metaclass on python 3.7 :(
|
||||
# pylint: disable=undefined-variable
|
||||
|
||||
"""
|
||||
Support for the Secure Element Access Control, specifically the ARA-M inside an UICC.
|
||||
"""
|
||||
|
||||
#
|
||||
# Copyright (C) 2021 Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
|
||||
from construct import *
|
||||
from construct import Optional as COptional
|
||||
from pySim.construct import *
|
||||
from pySim.filesystem import *
|
||||
from pySim.tlv import *
|
||||
|
||||
# various BER-TLV encoded Data Objects (DOs)
|
||||
|
||||
class AidRefDO(BER_TLV_IE, tag=0x4f):
|
||||
# SEID v1.1 Table 6-3
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
class AidRefEmptyDO(BER_TLV_IE, tag=0xc0):
|
||||
# SEID v1.1 Table 6-3
|
||||
pass
|
||||
|
||||
class DevAppIdRefDO(BER_TLV_IE, tag=0xc1):
|
||||
# SEID v1.1 Table 6-4
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
class PkgRefDO(BER_TLV_IE, tag=0xca):
|
||||
# Android UICC Carrier Privileges specific extension, see https://source.android.com/devices/tech/config/uicc
|
||||
_construct = Struct('package_name_string'/GreedyString("ascii"))
|
||||
|
||||
class RefDO(BER_TLV_IE, tag=0xe1, nested=[AidRefDO,AidRefEmptyDO,DevAppIdRefDO,PkgRefDO]):
|
||||
# SEID v1.1 Table 6-5
|
||||
pass
|
||||
|
||||
class ApduArDO(BER_TLV_IE, tag=0xd0):
|
||||
# SEID v1.1 Table 6-8
|
||||
def _from_bytes(self, do:bytes):
|
||||
if len(do) == 1:
|
||||
if do[0] == 0x00:
|
||||
self.decoded = {'generic_access_rule': 'never'}
|
||||
return self.decoded
|
||||
elif do[0] == 0x01:
|
||||
self.decoded = {'generic_access_rule': 'always'}
|
||||
return self.decoded
|
||||
else:
|
||||
return ValueError('Invalid 1-byte generic APDU access rule')
|
||||
else:
|
||||
if len(do) % 8:
|
||||
return ValueError('Invalid non-modulo-8 length of APDU filter: %d' % len(do))
|
||||
self.decoded['apdu_filter'] = []
|
||||
offset = 0
|
||||
while offset < len(do):
|
||||
self.decoded['apdu_filter'] += {'header': b2h(do[offset:offset+4]),
|
||||
'mask': b2h(do[offset+4:offset+8])}
|
||||
self.decoded = res
|
||||
return res
|
||||
def _to_bytes(self):
|
||||
if 'generic_access_rule' in self.decoded:
|
||||
if self.decoded['generic_access_rule'] == 'never':
|
||||
return b'\x00'
|
||||
elif self.decoded['generic_access_rule'] == 'always':
|
||||
return b'\x01'
|
||||
else:
|
||||
return ValueError('Invalid 1-byte generic APDU access rule')
|
||||
else:
|
||||
if not 'apdu_filter' in self.decoded:
|
||||
return ValueError('Invalid APDU AR DO')
|
||||
filters = self.decoded['apdu_filter']
|
||||
res = b''
|
||||
for f in filters:
|
||||
if not 'header' in f or not 'mask' in f:
|
||||
return ValueError('APDU filter must contain header and mask')
|
||||
header_b = h2b(f['header'])
|
||||
mask_b = h2b(f['mask'])
|
||||
if len(header_b) != 4 or len(mask_b) != 4:
|
||||
return ValueError('APDU filter header and mask must each be 4 bytes')
|
||||
res += header_b + mask_b
|
||||
return res
|
||||
|
||||
class NfcArDO(BER_TLV_IE, tag=0xd1):
|
||||
# SEID v1.1 Table 6-9
|
||||
_construct = Struct('nfc_event_access_rule'/Enum(Int8ub, never=0, always=1))
|
||||
|
||||
class PermArDO(BER_TLV_IE, tag=0xdb):
|
||||
# Android UICC Carrier Privileges specific extension, see https://source.android.com/devices/tech/config/uicc
|
||||
_construct = Struct('permissions'/HexAdapter(Bytes(8)))
|
||||
|
||||
class ArDO(BER_TLV_IE, tag=0xe3, nested=[ApduArDO, NfcArDO, PermArDO]):
|
||||
# SEID v1.1 Table 6-7
|
||||
pass
|
||||
|
||||
class RefArDO(BER_TLV_IE, tag=0xe2, nested=[RefDO, ArDO]):
|
||||
# SEID v1.1 Table 6-6
|
||||
pass
|
||||
|
||||
class ResponseAllRefArDO(BER_TLV_IE, tag=0xff40, nested=[RefArDO]):
|
||||
# SEID v1.1 Table 4-2
|
||||
pass
|
||||
|
||||
class ResponseArDO(BER_TLV_IE, tag=0xff50, nested=[ArDO]):
|
||||
# SEID v1.1 Table 4-3
|
||||
pass
|
||||
|
||||
class ResponseRefreshTagDO(BER_TLV_IE, tag=0xdf20):
|
||||
# SEID v1.1 Table 4-4
|
||||
_construct = Struct('refresh_tag'/HexAdapter(Bytes(8)))
|
||||
|
||||
class DeviceInterfaceVersionDO(BER_TLV_IE, tag=0xe6):
|
||||
# SEID v1.1 Table 6-12
|
||||
_construct = Struct('major'/Int8ub, 'minor'/Int8ub, 'patch'/Int8ub)
|
||||
|
||||
class DeviceConfigDO(BER_TLV_IE, tag=0xe4, nested=[DeviceInterfaceVersionDO]):
|
||||
# SEID v1.1 Table 6-10
|
||||
pass
|
||||
|
||||
class ResponseDeviceConfigDO(BER_TLV_IE, tag=0xff7f, nested=[DeviceConfigDO]):
|
||||
# SEID v1.1 Table 5-14
|
||||
pass
|
||||
|
||||
class AramConfigDO(BER_TLV_IE, tag=0xe5, nested=[DeviceInterfaceVersionDO]):
|
||||
# SEID v1.1 Table 6-11
|
||||
pass
|
||||
|
||||
class ResponseAramConfigDO(BER_TLV_IE, tag=0xdf21, nested=[AramConfigDO]):
|
||||
# SEID v1.1 Table 4-5
|
||||
pass
|
||||
|
||||
class CommandStoreRefArDO(BER_TLV_IE, tag=0xf0, nested=[RefArDO]):
|
||||
# SEID v1.1 Table 5-2
|
||||
pass
|
||||
|
||||
class CommandDelete(BER_TLV_IE, tag=0xf1, nested=[AidRefDO, AidRefEmptyDO, RefDO, RefArDO]):
|
||||
# SEID v1.1 Table 5-4
|
||||
pass
|
||||
|
||||
class CommandUpdateRefreshTagDO(BER_TLV_IE, tag=0xf2):
|
||||
# SEID V1.1 Table 5-6
|
||||
pass
|
||||
|
||||
class CommandRegisterClientAidsDO(BER_TLV_IE, tag=0xf7, nested=[AidRefDO, AidRefEmptyDO]):
|
||||
# SEID v1.1 Table 5-7
|
||||
pass
|
||||
|
||||
class CommandGet(BER_TLV_IE, tag=0xf3, nested=[AidRefDO, AidRefEmptyDO]):
|
||||
# SEID v1.1 Table 5-8
|
||||
pass
|
||||
|
||||
class CommandGetAll(BER_TLV_IE, tag=0xf4):
|
||||
# SEID v1.1 Table 5-9
|
||||
pass
|
||||
|
||||
class CommandGetClientAidsDO(BER_TLV_IE, tag=0xf6):
|
||||
# SEID v1.1 Table 5-10
|
||||
pass
|
||||
|
||||
class CommandGetNext(BER_TLV_IE, tag=0xf5):
|
||||
# SEID v1.1 Table 5-11
|
||||
pass
|
||||
|
||||
class CommandGetDeviceConfigDO(BER_TLV_IE, tag=0xf8):
|
||||
# SEID v1.1 Table 5-12
|
||||
pass
|
||||
|
||||
class ResponseAracAidDO(BER_TLV_IE, tag=0xff70, nested=[AidRefDO, AidRefEmptyDO]):
|
||||
# SEID v1.1 Table 5-13
|
||||
pass
|
||||
|
||||
class BlockDO(BER_TLV_IE, tag=0xe7):
|
||||
# SEID v1.1 Table 6-13
|
||||
_construct = Struct('offset'/Int16ub, 'length'/Int8ub)
|
||||
|
||||
|
||||
# SEID v1.1 Table 4-1
|
||||
class GetCommandDoCollection(TLV_IE_Collection, nested=[RefDO, DeviceConfigDO]):
|
||||
pass
|
||||
|
||||
# SEID v1.1 Table 4-2
|
||||
class GetResponseDoCollection(TLV_IE_Collection, nested=[ResponseAllRefArDO, ResponseArDO,
|
||||
ResponseRefreshTagDO, ResponseAramConfigDO]):
|
||||
pass
|
||||
|
||||
# SEID v1.1 Table 5-1
|
||||
class StoreCommandDoCollection(TLV_IE_Collection,
|
||||
nested=[BlockDO, CommandStoreRefArDO, CommandDelete,
|
||||
CommandUpdateRefreshTagDO, CommandRegisterClientAidsDO,
|
||||
CommandGet, CommandGetAll, CommandGetClientAidsDO,
|
||||
CommandGetNext, CommandGetDeviceConfigDO]):
|
||||
pass
|
||||
|
||||
|
||||
# SEID v1.1 Section 5.1.2
|
||||
class StoreResponseDoCollection(TLV_IE_Collection,
|
||||
nested=[ResponseAllRefArDO, ResponseAracAidDO, ResponseDeviceConfigDO]):
|
||||
pass
|
||||
|
||||
class ADF_ARAM(CardADF):
|
||||
def __init__(self, aid='a00000015141434c00', name='ADF.ARA-M', fid=None, sfid=None,
|
||||
desc='ARA-M Application'):
|
||||
super().__init__(aid=aid, fid=fid, sfid=sfid, name=name, desc=desc)
|
||||
self.shell_commands += [self.AddlShellCommands()]
|
||||
files = []
|
||||
self.add_files(files)
|
||||
|
||||
@staticmethod
|
||||
def xceive_apdu_tlv(tp, hdr:Hexstr, cmd_do, resp_cls, exp_sw='9000'):
|
||||
"""Transceive an APDU with the card, transparently encoding the command data from TLV
|
||||
and decoding the response data tlv."""
|
||||
if cmd_do:
|
||||
cmd_do_enc = cmd_do.to_ie()
|
||||
cmd_do_len = len(cmd_do_enc)
|
||||
if cmd_do_len > 255:
|
||||
return ValueError('DO > 255 bytes not supported yet')
|
||||
else:
|
||||
cmd_do_enc = b''
|
||||
cmd_do_len = 0
|
||||
c_apdu = hdr + ('%02x' % cmd_do_len) + b2h(cmd_do_enc)
|
||||
(data, sw) = tp.send_apdu_checksw(c_apdu, exp_sw)
|
||||
if data:
|
||||
if resp_cls:
|
||||
resp_do = resp_cls()
|
||||
resp_do.from_tlv(h2b(data))
|
||||
return resp_do
|
||||
else:
|
||||
return data
|
||||
else:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def store_data(tp, do) -> bytes:
|
||||
"""Build the Command APDU for STORE DATA."""
|
||||
return ADF_ARAM.xceive_apdu_tlv(tp, '80e29000', do, StoreResponseDoCollection)
|
||||
|
||||
@staticmethod
|
||||
def get_all(tp):
|
||||
return ADF_ARAM.xceive_apdu_tlv(tp, '80caff40', None, GetResponseDoCollection)
|
||||
|
||||
@staticmethod
|
||||
def get_config(tp, v_major=0, v_minor=0, v_patch=1):
|
||||
cmd_do = DeviceConfigDO()
|
||||
cmd_do.from_dict([{'DeviceInterfaceVersionDO': {'major': v_major, 'minor': v_minor, 'patch': v_patch }}])
|
||||
return ADF_ARAM.xceive_apdu_tlv(tp, '80cadf21', cmd_do, ResponseAramConfigDO)
|
||||
|
||||
@with_default_category('Application-Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
def __init(self):
|
||||
super().__init__()
|
||||
|
||||
def do_aram_get_all(self, opts):
|
||||
"""GET DATA [All] on the ARA-M Applet"""
|
||||
res_do = ADF_ARAM.get_all(self._cmd.card._scc._tp)
|
||||
if res_do:
|
||||
self._cmd.poutput_json(res_do.to_dict())
|
||||
|
||||
def do_aram_get_config(self, opts):
|
||||
"""GET DATA [Config] on the ARA-M Applet"""
|
||||
res_do = ADF_ARAM.get_config(self._cmd.card._scc._tp)
|
||||
if res_do:
|
||||
self._cmd.poutput_json(res_do.to_dict())
|
||||
|
||||
store_ref_ar_do_parse = argparse.ArgumentParser()
|
||||
# REF-DO
|
||||
store_ref_ar_do_parse.add_argument('--device-app-id', required=True, help='Identifies the specific device application that the rule appplies to. Hash of Certificate of Application Provider, or UUID. (20/32 hex bytes)')
|
||||
aid_grp = store_ref_ar_do_parse.add_mutually_exclusive_group()
|
||||
aid_grp.add_argument('--aid', help='Identifies the specific SE application for which rules are to be stored. Can be a partial AID, containing for example only the RID. (5-16 hex bytes)')
|
||||
aid_grp.add_argument('--aid-empty', action='store_true', help='No specific SE application, applies to all applications')
|
||||
store_ref_ar_do_parse.add_argument('--pkg-ref', help='Full Android Java package name (up to 127 chars ASCII)')
|
||||
# AR-DO
|
||||
apdu_grp = store_ref_ar_do_parse.add_mutually_exclusive_group()
|
||||
apdu_grp.add_argument('--apdu-never', action='store_true', help='APDU access is not allowed')
|
||||
apdu_grp.add_argument('--apdu-always', action='store_true', help='APDU access is allowed')
|
||||
apdu_grp.add_argument('--apdu-filter', help='APDU filter: 4 byte CLA/INS/P1/P2 followed by 4 byte mask (8 hex bytes)')
|
||||
nfc_grp = store_ref_ar_do_parse.add_mutually_exclusive_group()
|
||||
nfc_grp.add_argument('--nfc-always', action='store_true', help='NFC event access is allowed')
|
||||
nfc_grp.add_argument('--nfc-never', action='store_true', help='NFC event access is not allowed')
|
||||
store_ref_ar_do_parse.add_argument('--android-permissions', help='Android UICC Carrier Privilege Permissions (8 hex bytes)')
|
||||
|
||||
@cmd2.with_argparser(store_ref_ar_do_parse)
|
||||
def do_aram_store_ref_ar_do(self, opts):
|
||||
"""Perform STORE DATA [Command-Store-REF-AR-DO] to store a new access rule."""
|
||||
# REF
|
||||
ref_do_content = []
|
||||
if opts.aid:
|
||||
ref_do_content += [{'AidRefDO': opts.aid}]
|
||||
elif opts.aid_empty:
|
||||
ref_do_content += [{'AidRefEmptyDO': None}]
|
||||
ref_do_content += [{'DevAppIdRefDO': opts.device_app_id}]
|
||||
if opts.pkg_ref:
|
||||
ref_do_content += [{'PkgRefDO': opts.pkg_ref}]
|
||||
# AR
|
||||
ar_do_content = []
|
||||
if opts.apdu_never:
|
||||
ar_do_content += [{'ApduArDO': {'generic_access_rule': 'never'}}]
|
||||
elif opts.apdu_always:
|
||||
ar_do_content += [{'ApduArDO': {'generic_access_rule': 'always'}}]
|
||||
elif opts.apdu_filter:
|
||||
# TODO: multiple filters
|
||||
ar_do_content += [{'ApduArDO': {'apdu_filter': [opts.apdu_filter]}}]
|
||||
if opts.nfc_always:
|
||||
ar_do_content += [{'NfcArDO': {'nfc_event_access_rule': 'always'}}]
|
||||
elif opts.nfc_never:
|
||||
ar_do_content += [{'NfcArDO': {'nfc_event_access_rule': 'never'}}]
|
||||
if opts.android_permissions:
|
||||
ar_do_content += [{'PermArDO': {'permissions': opts.android_permissions}}]
|
||||
d = [{'RefArDO': [{ 'RefDO': ref_do_content}, {'ArDO': ar_do_content }]}]
|
||||
csrado = CommandStoreRefArDO()
|
||||
csrado.from_dict(d)
|
||||
res_do = ADF_ARAM.store_data(self._cmd.card._scc._tp, csrado)
|
||||
if res_do:
|
||||
self._cmd.poutput_json(res_do.to_dict())
|
||||
|
||||
def do_aram_delete_all(self, opts):
|
||||
"""Perform STORE DATA [Command-Delete[all]] to delete all access rules."""
|
||||
deldo = CommandDelete()
|
||||
res_do = ADF_ARAM.store_data(self._cmd.card._scc._tp, deldo)
|
||||
if res_do:
|
||||
self._cmd.poutput_json(res_do.to_dict())
|
||||
|
||||
|
||||
# SEAC v1.1 Section 4.1.2.2 + 5.1.2.2
|
||||
sw_aram = {
|
||||
'ARA-M': {
|
||||
'6381': 'Rule successfully stored but an access rule already exists',
|
||||
'6382': 'Rule successfully stored bu contained at least one unknown (discarded) BER-TLV',
|
||||
'6581': 'Memory Problem',
|
||||
'6700': 'Wrong Length in Lc',
|
||||
'6981': 'DO is not supported by the ARA-M/ARA-C',
|
||||
'6982': 'Security status not satisfied',
|
||||
'6984': 'Rules have been updated and must be read again / logical channels in use',
|
||||
'6985': 'Conditions not satisfied',
|
||||
'6a80': 'Incorrect values in the command data',
|
||||
'6a84': 'Rules have been updated and must be read again',
|
||||
'6a86': 'Incorrect P1 P2',
|
||||
'6a88': 'Referenced data not found',
|
||||
'6a89': 'Conflicting access rule already exists in the Secure Element',
|
||||
'6d00': 'Invalid instruction',
|
||||
'6e00': 'Invalid class',
|
||||
}
|
||||
}
|
||||
|
||||
class CardApplicationARAM(CardApplication):
|
||||
def __init__(self):
|
||||
super().__init__('ARA-M', adf=ADF_ARAM(), sw=sw_aram)
|
Loading…
Reference in New Issue