Repository: eclipse/leshan (java)

Issue:
Leshan does not decode SenML-CBOR/JSON data value (vd/8) correctly
Hi Leshan Team

I'm trying out the AVSystem's Coiote LwM2M stack (both bootstrap and device management) using Leshan client demo.
When I connect the client to the bootstrap server, I get this error when the server sends back the device manager info to connect to:

```
2021-03-19 14:26:48,006 ERROR LwM2mCoapResource - Exception while handling request [CON-PUT    MID= 7581, Token=3EA719C0258E200B, OptionSet={"Uri-Path":"0", "Content-Format":"application/senml+cbor"},
8d a3 21 63 2f 30 2f 00 63 31 2f 30 03 78 23 63 6f 61 70 73 3a 2f 2f 6c 77 6d 32 6d 2d 74 65 73
74 2e 61 76 73 79 73 74 65 6d 2e 69 6f 3a 35 36 39 34 a2 00 63 31 2f 32 02 00 a2 00 63 31 2f 33
08 47 74 65 73 74 2d 6a 62 a2 00 63 31 2f 35 08 58 18 58 59 37 6d 73 43 46 4f 30 47 61 67 64 34
46 77 6b 56 36 6f 36 6a 6b 33 a2 00 63 31 2f 34 08 40 a2 00 63 31 2f 31 04 f5 a2 00 63 32 2f 30
03 78 23 63 6f 61 70 73 3a 2f 2f 6c 77 6d 32 6d 2d 74 65 73 74 2e 61 76 73 79 73 74 65 6d 2e 69
6f 3a 35 36 38 34 a2 00 63 32 2f 32 02 00 a2 00 63 32 2f 33 08 47 74 65 73 74 2d 6a 62 a2 00 63
32 2f 35 08 58 18 6b 72 56 63 43 62 6e 55 61 42 56 72 53 35 48 59 32 52 42 51 4e 72 52 76 a2 00
63 32 2f 34 08 40 a2 00 63 32 2f 31 04 f4 a2 00 64 32 2f 31 30 02 01 ] on the resource /0 from Identity lwm2m-test.avsystem.io/35.241.179.127:5694[psk=test-jb]
org.eclipse.leshan.core.util.json.JsonException: Invalid SenML record : record must have a value (v,vb,vlo,vd,vs) : {0: "1/3", 8: h'746573742D6A62'}
	at org.eclipse.leshan.senml.cbor.upokecenter.SenMLCborPackSerDes.deserializeFromCbor(SenMLCborPackSerDes.java:117)
	at org.eclipse.leshan.senml.cbor.upokecenter.SenMLCborUpokecenterEncoderDecoder.fromSenML(SenMLCborUpokecenterEncoderDecoder.java:72)
	at org.eclipse.leshan.core.node.codec.senml.LwM2mNodeSenMLDecoder.decode(LwM2mNodeSenMLDecoder.java:68)
	at org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeDecoder.decode(DefaultLwM2mNodeDecoder.java:127)
	at org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeDecoder.decode(DefaultLwM2mNodeDecoder.java:109)
	at org.eclipse.leshan.client.californium.object.ObjectResource.handlePUT(ObjectResource.java:228)
	at org.eclipse.californium.core.CoapResource.handleRequest(CoapResource.java:228)
	at org.eclipse.leshan.core.californium.LwM2mCoapResource.handleRequest(LwM2mCoapResource.java:51)
	at org.eclipse.californium.core.server.ServerMessageDeliverer.deliverRequest(ServerMessageDeliverer.java:106)
	at org.eclipse.californium.core.network.stack.BaseCoapStack$StackTopAdapter.receiveRequest(BaseCoapStack.java:204)
	at org.eclipse.californium.core.network.stack.AbstractLayer.receiveRequest(AbstractLayer.java:84)
	at org.eclipse.californium.core.network.stack.AbstractLayer.receiveRequest(AbstractLayer.java:84)
	at org.eclipse.californium.core.network.stack.BlockwiseLayer.receiveRequest(BlockwiseLayer.java:413)
	at org.eclipse.californium.core.network.stack.ReliabilityLayer.receiveRequest(ReliabilityLayer.java:275)
	at org.eclipse.californium.core.network.stack.AbstractLayer.receiveRequest(AbstractLayer.java:84)
	at org.eclipse.californium.core.network.stack.BaseCoapStack.receiveRequest(BaseCoapStack.java:137)
	at org.eclipse.californium.core.network.CoapEndpoint$1.receiveRequest(CoapEndpoint.java:309)
	at org.eclipse.californium.core.network.UdpMatcher$2.run(UdpMatcher.java:270)
	at org.eclipse.californium.elements.util.SerialExecutor$1.run(SerialExecutor.java:289)
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:836)
2021-03-19 14:28:19,770 INFO DefaultRegistrationEngine - Bootstrap sequence aborted: Timeout.
```

I asked about it and the guys at AV Systems say that it's probably a bug on Leshan's side as it is expecting text labels instead of numeric ones.
I've had the same result with *master* and *demo_v2* branches and  *leshan-2.0.0-M2* tag


BR

Jacinto

Pull request description:
Fix issue about decoding data value (vd/8) with SenML_CBOR.
This aims to fix #984 

Patch:
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLDecoder.java
index 2faf0a91..a904df84 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLDecoder.java
@@ -43,7 +43,7 @@ import org.eclipse.leshan.core.node.codec.CodecException;
 import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeDecoder;
 import org.eclipse.leshan.core.node.codec.MultiNodeDecoder;
 import org.eclipse.leshan.core.node.codec.TimestampedNodeDecoder;
-import org.eclipse.leshan.core.util.Base64;
+import org.eclipse.leshan.core.util.Hex;
 import org.eclipse.leshan.core.util.datatype.NumberUtil;
 import org.eclipse.leshan.core.util.datatype.ULong;
 import org.eclipse.leshan.senml.SenMLDecoder;
@@ -96,8 +96,8 @@ public class LwM2mNodeSenMLDecoder implements TimestampedNodeDecoder, MultiNodeD
             // Parse records and create node
             return (T) parseRecords(resolvedRecords, path, model, nodeClass);
         } catch (SenMLException e) {
-            String jsonStrValue = content != null ? new String(content) : "";
-            throw new CodecException(e, "Unable to decode node[path:%s] : %s", path, jsonStrValue, e);
+            String hexValue = content != null ? Hex.encodeHexString(content) : "";
+            throw new CodecException(e, "Unable to decode node[path:%s] : %s", path, hexValue, e);
         }
     }
 
@@ -142,11 +142,9 @@ public class LwM2mNodeSenMLDecoder implements TimestampedNodeDecoder, MultiNodeD
                 }
             }
             return nodes;
-        } catch (
-
-        SenMLException e) {
-            String jsonStrValue = content != null ? new String(content) : "";
-            throw new CodecException(e, "Unable to decode nodes[path:%s] : %s", paths, jsonStrValue, e);
+        } catch (SenMLException e) {
+            String hexValue = content != null ? Hex.encodeHexString(content) : "";
+            throw new CodecException(e, "Unable to decode nodes[path:%s] : %s", paths, hexValue, e);
         }
     }
 
@@ -170,8 +168,8 @@ public class LwM2mNodeSenMLDecoder implements TimestampedNodeDecoder, MultiNodeD
             }
             return timestampedNodes;
         } catch (SenMLException e) {
-            String jsonStrValue = content != null ? new String(content) : "";
-            throw new CodecException(e, "Unable to decode node[path:%s] : %s", path, jsonStrValue, e);
+            String hexValue = content != null ? Hex.encodeHexString(content) : "";
+            throw new CodecException(e, "Unable to decode node[path:%s] : %s", path, hexValue, e);
         }
     }
 
@@ -482,9 +480,7 @@ public class LwM2mNodeSenMLDecoder implements TimestampedNodeDecoder, MultiNodeD
             case TIME:
                 return new Date(numberToLong((Number) value) * 1000L);
             case OPAQUE:
-                // If the Resource data type is opaque the string value
-                // holds the Base64 encoded representation of the Resource
-                return Base64.decodeBase64((String) value);
+                return value;
             case STRING:
                 return value;
             case OBJLNK:
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLEncoder.java
index 0aa824cd..864320b7 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLEncoder.java
@@ -35,7 +35,6 @@ import org.eclipse.leshan.core.node.codec.CodecException;
 import org.eclipse.leshan.core.node.codec.LwM2mValueConverter;
 import org.eclipse.leshan.core.node.codec.MultiNodeEncoder;
 import org.eclipse.leshan.core.node.codec.TimestampedNodeEncoder;
-import org.eclipse.leshan.core.util.Base64;
 import org.eclipse.leshan.core.util.Validate;
 import org.eclipse.leshan.senml.SenMLEncoder;
 import org.eclipse.leshan.senml.SenMLException;
@@ -292,7 +291,7 @@ public class LwM2mNodeSenMLEncoder implements TimestampedNodeEncoder, MultiNodeE
                 record.setFloatValue((((Date) value).getTime() / 1000L));
                 break;
             case OPAQUE:
-                record.setStringValue(Base64.encodeBase64String((byte[]) value));
+                record.setOpaqueValue((byte[]) value);
                 break;
             case OBJLNK:
                 try {
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLRecord.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLRecord.java
index 51d3d092..8a49cf19 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLRecord.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLRecord.java
@@ -14,8 +14,11 @@
 
 package org.eclipse.leshan.senml;
 
+import java.util.Arrays;
+
 import org.eclipse.leshan.core.model.ResourceModel;
 import org.eclipse.leshan.core.model.ResourceModel.Type;
+import org.eclipse.leshan.core.util.Hex;
 
 /**
  * The class representing the SenML Record.
@@ -157,7 +160,7 @@ public class SenMLRecord {
         result = prime * result + ((name == null) ? 0 : name.hashCode());
         result = prime * result + ((objectLinkValue == null) ? 0 : objectLinkValue.hashCode());
         result = prime * result + ((stringValue == null) ? 0 : stringValue.hashCode());
-        result = prime * result + ((opaqueValue == null) ? 0 : opaqueValue.hashCode());
+        result = prime * result + ((opaqueValue == null) ? 0 : Arrays.hashCode(opaqueValue));
         return result;
     }
 
@@ -221,7 +224,7 @@ public class SenMLRecord {
         if (opaqueValue == null) {
             if (other.opaqueValue != null)
                 return false;
-        } else if (!opaqueValue.equals(other.opaqueValue))
+        } else if (!Arrays.equals(opaqueValue, other.opaqueValue))
             return false;
 
         return true;
@@ -231,6 +234,7 @@ public class SenMLRecord {
     public String toString() {
         return String.format(
                 "SenMLRecord [baseName=%s, baseTime=%d, name=%s, time=%d, floatValue=%s, booleanValue=%s, objectLinkValue=%s, stringValue=%s, opaque=%s]",
-                baseName, baseTime, name, time, floatValue, booleanValue, objectLinkValue, stringValue, opaqueValue);
+                baseName, baseTime, name, time, floatValue, booleanValue, objectLinkValue, stringValue,
+                opaqueValue != null ? Hex.encodeHexString(opaqueValue) : "null");
     }
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/jackson/SenMLCborPackSerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/jackson/SenMLCborPackSerDes.java
index fe79e88b..fee3921a 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/jackson/SenMLCborPackSerDes.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/jackson/SenMLCborPackSerDes.java
@@ -15,12 +15,12 @@
 package org.eclipse.leshan.senml.cbor.jackson;
 
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.util.Iterator;
 
 import org.eclipse.leshan.core.model.ResourceModel.Type;
-import org.eclipse.leshan.core.util.Base64;
 import org.eclipse.leshan.core.util.datatype.ULong;
 import org.eclipse.leshan.senml.SenMLException;
 import org.eclipse.leshan.senml.SenMLPack;
@@ -83,8 +83,12 @@ public class SenMLCborPackSerDes {
             }
 
             JsonNode vd = o.get("8");
-            if (vd != null && vd.isTextual()) {
-                record.setOpaqueValue(Base64.decodeBase64(vd.asText()));
+            if (vd != null && vd.isBinary()) {
+                try {
+                    record.setOpaqueValue(vd.binaryValue());
+                } catch (IOException e) {
+                    throw new SenMLException("Invalid SenML record : unable to get binary value for %s", o);
+                }
                 hasValue = true;
             }
 
@@ -192,6 +196,7 @@ public class SenMLCborPackSerDes {
                     case OPAQUE:
                         generator.writeFieldId(8);
                         generator.writeBinary(record.getOpaqueValue());
+                        break;
                     case STRING:
                         generator.writeFieldId(3);
                         generator.writeString(record.getStringValue());
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/upokecenter/SenMLCborPackSerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/upokecenter/SenMLCborPackSerDes.java
index a4d143b2..ef30394e 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/upokecenter/SenMLCborPackSerDes.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/upokecenter/SenMLCborPackSerDes.java
@@ -23,7 +23,6 @@ import java.math.BigInteger;
 import java.util.Collection;
 
 import org.eclipse.leshan.core.model.ResourceModel.Type;
-import org.eclipse.leshan.core.util.Base64;
 import org.eclipse.leshan.core.util.datatype.NumberUtil;
 import org.eclipse.leshan.core.util.datatype.ULong;
 import org.eclipse.leshan.senml.SenMLException;
@@ -126,8 +125,8 @@ public class SenMLCborPackSerDes {
                 }
 
                 CBORObject vd = o.get(8);
-                if (vd != null && vd.getType() == CBORType.TextString) {
-                    record.setOpaqueValue(Base64.decodeBase64(vd.AsString()));
+                if (vd != null && vd.getType() == CBORType.ByteString) {
+                    record.setOpaqueValue(vd.GetByteString());
                     hasValue = true;
                 }
 
@@ -202,6 +201,7 @@ public class SenMLCborPackSerDes {
                         break;
                     case OPAQUE:
                         cborRecord.Add(8, record.getOpaqueValue());
+                        break;
                     case STRING:
                         cborRecord.Add(3, record.getStringValue());
                         break;
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/json/jackson/SenMLJsonRecordSerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/json/jackson/SenMLJsonRecordSerDes.java
index e30b1600..8ed50c45 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/json/jackson/SenMLJsonRecordSerDes.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/json/jackson/SenMLJsonRecordSerDes.java
@@ -106,6 +106,7 @@ public class SenMLJsonRecordSerDes extends JacksonJsonSerDes<SenMLRecord> {
                 break;
             case OPAQUE:
                 jsonObj.put("vd", Base64.encodeBase64String(record.getOpaqueValue()));
+                break;
             case STRING:
                 jsonObj.put("vs", record.getStringValue());
                 break;
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/json/minimaljson/SenMLJsonPackSerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/json/minimaljson/SenMLJsonPackSerDes.java
index f5068bb3..884bbc4f 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/json/minimaljson/SenMLJsonPackSerDes.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/json/minimaljson/SenMLJsonPackSerDes.java
@@ -61,6 +61,7 @@ public class SenMLJsonPackSerDes {
                     break;
                 case OPAQUE:
                     jsonObj.add("vd", Base64.encodeBase64String(record.getOpaqueValue()));
+                    break;
                 case STRING:
                     jsonObj.add("vs", record.getStringValue());
                     break;


Test patch:
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoderTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoderTest.java
index ceb3484b..928b3792 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoderTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoderTest.java
@@ -1167,4 +1167,29 @@ public class LwM2mNodeDecoderTest {
         // Decode
         decoder.decodePaths(b.toString().getBytes(), ContentFormat.SENML_JSON);
     }
+
+    @Test
+    public void senml_json_decode_opaque_resource() {
+        byte[] json = "[{\"bn\":\"/0/0/3\",\"vd\":\"q83v\"}]".getBytes(); // q83v is base64 of ABCDE
+        LwM2mResource oResource = (LwM2mResource) decoder.decode(json, ContentFormat.SENML_JSON,
+                new LwM2mPath("/0/0/3"), model);
+
+        byte[] bytes = Hex.decodeHex("ABCDEF".toCharArray());
+        LwM2mResource expected = LwM2mSingleResource.newBinaryResource(3, bytes);
+
+        Assert.assertEquals(expected, oResource);
+    }
+
+    @Test
+    public void senml_cbor_decode_opaque_resource() {
+        // value : [{-2: "/0/0/3", 8: h'ABCDEF'}]
+        byte[] cbor = Hex.decodeHex("81a221662f302f302f330843abcdef".toCharArray());
+        LwM2mResource oResource = (LwM2mResource) decoder.decode(cbor, ContentFormat.SENML_CBOR,
+                new LwM2mPath("/0/0/3"), model);
+
+        byte[] bytes = Hex.decodeHex("ABCDEF".toCharArray());
+        LwM2mResource expected = LwM2mSingleResource.newBinaryResource(3, bytes);
+
+        Assert.assertEquals(expected, oResource);
+    }
 }
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeEncoderTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeEncoderTest.java
index bcc78487..6324ba5d 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeEncoderTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeEncoderTest.java
@@ -38,8 +38,10 @@ import org.eclipse.leshan.core.node.LwM2mResource;
 import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.node.LwM2mSingleResource;
 import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
+import org.eclipse.leshan.core.node.codec.senml.LwM2mNodeSenMLEncoder;
 import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.core.util.Hex;
+import org.eclipse.leshan.senml.cbor.upokecenter.SenMLCborUpokecenterEncoderDecoder;
 import org.junit.Assert;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -55,7 +57,14 @@ public class LwM2mNodeEncoderTest {
     @BeforeClass
     public static void loadModel() {
         model = new StaticModel(ObjectLoader.loadDefault());
-        encoder = new DefaultLwM2mNodeEncoder();
+
+        // keep CBOR order to be able to test.
+        Map<ContentFormat, NodeEncoder> defaultNodeEncoders = DefaultLwM2mNodeEncoder.getDefaultNodeEncoders(false);
+        defaultNodeEncoders.put(ContentFormat.SENML_CBOR,
+                new LwM2mNodeSenMLEncoder(new SenMLCborUpokecenterEncoderDecoder(true, false)));
+
+        encoder = new DefaultLwM2mNodeEncoder(defaultNodeEncoders, DefaultLwM2mNodeEncoder.getDefaultPathEncoder(),
+                new LwM2mValueChecker());
     }
 
     @Test
@@ -434,4 +443,24 @@ public class LwM2mNodeEncoderTest {
         b.append("{\"n\":\"/4/0/2\"}]");
         Assert.assertEquals(b.toString(), new String(res));
     }
+
+    @Test
+    public void senml_json_encode_opaque_resource() {
+        byte[] bytes = Hex.decodeHex("ABCDEF".toCharArray());
+        LwM2mResource oResource = LwM2mSingleResource.newBinaryResource(3, bytes);
+        byte[] json = encoder.encode(oResource, ContentFormat.SENML_JSON, new LwM2mPath("/0/0/3"), model);
+
+        String expected = "[{\"bn\":\"/0/0/3\",\"vd\":\"q83v\"}]"; // q83v is base64 of ABCDE
+        Assert.assertEquals(expected, new String(json));
+    }
+
+    @Test
+    public void senml_cbor_encode_opaque_resource() {
+        byte[] bytes = Hex.decodeHex("ABCDEF".toCharArray());
+        LwM2mResource oResource = LwM2mSingleResource.newBinaryResource(3, bytes);
+        byte[] cbor = encoder.encode(oResource, ContentFormat.SENML_CBOR, new LwM2mPath("/0/0/3"), model);
+        // value : [{-2: "/0/0/3", 8: h'ABCDEF'}]
+        String expected = "81a221662f302f302f330843abcdef";
+        Assert.assertEquals(expected, Hex.encodeHexString(cbor));
+    }
 }
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/AbstractSenMLTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/AbstractSenMLTest.java
index d1e7b811..2a7cb3f7 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/AbstractSenMLTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/AbstractSenMLTest.java
@@ -101,4 +101,15 @@ public abstract class AbstractSenMLTest {
 
         return pack;
     }
+
+    protected SenMLPack getPackWithSingleOpaqueValue(String path, byte[] value) {
+        SenMLPack pack = new SenMLPack();
+
+        SenMLRecord r = new SenMLRecord();
+        r.setBaseName(path);
+        r.setOpaqueValue(value);
+        pack.addRecord(r);
+
+        return pack;
+    }
 }
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLCborSerializerTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLCborSerializerTest.java
index 14fbc729..42ddf95d 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLCborSerializerTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLCborSerializerTest.java
@@ -67,4 +67,24 @@ public class SenMLCborSerializerTest extends AbstractSenMLTest {
         SenMLPack pack = decoder.fromSenML(Hex.decodeHex(givenSenMLCborExample().toCharArray()));
         SenMLTestUtil.assertSenMLPackEquals(givenDeviceObjectInstance(), pack);
     }
+
+    @Test
+    public void deserialize_opaque_object_() throws Exception {
+        // value : [{-2: "/0/0/3", 8: h'ABCDEF'}]
+        byte[] cbor = Hex.decodeHex("81a221662f302f302f330843abcdef".toCharArray());
+        SenMLPack pack = decoder.fromSenML(cbor);
+
+        SenMLTestUtil.assertSenMLPackEquals(
+                getPackWithSingleOpaqueValue("/0/0/3", Hex.decodeHex("ABCDEF".toCharArray())), pack);
+    }
+
+    @Test
+    public void serialize_opaque_object_() throws Exception {
+        SenMLPack pack = getPackWithSingleOpaqueValue("/0/0/3", Hex.decodeHex("ABCDEF".toCharArray()));
+        byte[] cbor = encoder.toSenML(pack);
+
+        // value : [{-2: "/0/0/3", 8: h'ABCDEF'}]
+        String expected = "81a221662f302f302f330843abcdef";
+        Assert.assertEquals(expected, Hex.encodeHexString(cbor));
+    }
 }
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLJsonSerDesTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLJsonSerDesTest.java
index dc5068c7..fc4f37db 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLJsonSerDesTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLJsonSerDesTest.java
@@ -19,6 +19,7 @@ import static org.junit.Assert.assertEquals;
 import java.util.Arrays;
 import java.util.Collection;
 
+import org.eclipse.leshan.core.util.Hex;
 import org.eclipse.leshan.senml.SenMLDecoder;
 import org.eclipse.leshan.senml.SenMLEncoder;
 import org.eclipse.leshan.senml.SenMLException;
@@ -75,4 +76,22 @@ public class SenMLJsonSerDesTest extends AbstractSenMLTest {
         assertEquals(givenSenMLJsonExample(), new String(json));
     }
 
+    @Test
+    public void deserialize_opaque_object_() throws Exception {
+        byte[] json = "[{\"bn\":\"/0/0/3\",\"vd\":\"q83v\"}]".getBytes(); // q83v is base64 of ABCDE
+        SenMLPack pack = decoder.fromSenML(json);
+
+        SenMLTestUtil.assertSenMLPackEquals(
+                getPackWithSingleOpaqueValue("/0/0/3", Hex.decodeHex("ABCDEF".toCharArray())), pack);
+    }
+
+    @Test
+    public void serialize_opaque_object_() throws Exception {
+        SenMLPack pack = getPackWithSingleOpaqueValue("/0/0/3", Hex.decodeHex("ABCDEF".toCharArray()));
+        byte[] json = encoder.toSenML(pack);
+
+        String expected = "[{\"bn\":\"/0/0/3\",\"vd\":\"q83v\"}]"; // q83v is base64 of ABCDE
+        assertEquals(expected, new String(json));
+    }
+
 }