Skip to content

Commit

Permalink
validate encoded data (#544)
Browse files Browse the repository at this point in the history
* validate encoded data

* u

* c
  • Loading branch information
felicio authored Apr 8, 2024
1 parent 98741fb commit f9af5e0
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/bright-students-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@status-im/js': patch
---

validate encoded data
157 changes: 156 additions & 1 deletion packages/status-js/src/utils/encode-url-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,34 @@ describe('Encode URL data', () => {
expect(decodedData).toEqual(data)
})

test('should encode and decode channel', () => {
test('should throw for invalid community data', () => {
expect(() => {
const encodedData = 'Ow=='
decodeCommunityURLData(encodedData)
}).toThrowError()
})

test('should throw for unsupported data length', () => {
expect(() => {
const encodedData =
'G2QBQJwFdqwxrBnNb57kP0irrJpuouIjS1WZqHS6A2txojsUHidyu3evaAO3GQQku5NCQXiwAYchBIMNyptts=MD9bZAwoTasraIMkjbS1uAD7oxsAQ53OAmQWCefyBuuXlAu6J7eKQRQhgg5tan75fFp9jwGIjBLbGhnyUht2qj5GWlSBp7_OXsHxgnr21xA2HgR9VGYYikQJA4tcQHDrQzg_ARC9KiOVDD6vgTCM9_CN0HJ1zxwP3w6nzgkDTNuvDCFD3Clqo6Cf_UNY2cNRlKTqj86G4gC2dUNSApwiq72BdGTtrleiRFPUhCbTRbmEG4YwFOs4EjBdJHHRiqjS5GYGc1dAdgcGr2BQ==============================================================================================================================================='
decodeCommunityURLData(encodedData)
}).toThrowErrorMatchingInlineSnapshot(`
"[
{
\\"code\\": \\"too_big\\",
\\"maximum\\": 500,
\\"type\\": \\"string\\",
\\"inclusive\\": true,
\\"exact\\": false,
\\"message\\": \\"String must contain at most 500 character(s)\\",
\\"path\\": []
}
]"
`)
})

test('should encode and decode channel', () => {
const data = {
emoji: '🏴󠁧󠁢󠁥󠁮󠁧󠁿',
displayName: 'lorem-ipsum-dolore-nulla',
Expand All @@ -56,6 +83,70 @@ describe('Encode URL data', () => {
expect(decodedData).toEqual(data)
})

test('should throw for invalid channel data', () => {
expect(() => {
const encodedData = 'Ow=='
decodeChannelURLData(encodedData)
}).toThrowErrorMatchingInlineSnapshot(`
"[
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"displayName\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"description\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"emoji\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"color\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"object\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"community\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"uuid\\"
],
\\"message\\": \\"Required\\"
}
]"
`)
})

test('should encode and decode user', () => {
const data = {
displayName: 'Lorem ipsum dolore nulla',
Expand All @@ -72,4 +163,68 @@ describe('Encode URL data', () => {
)
expect(decodedData).toEqual(data)
})

test('should throw for invalid user data', () => {
expect(() => {
const encodedData = 'Ow=='
decodeChannelURLData(encodedData)
}).toThrowErrorMatchingInlineSnapshot(`
"[
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"displayName\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"description\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"emoji\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"color\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"object\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"community\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"uuid\\"
],
\\"message\\": \\"Required\\"
}
]"
`)
})
})
58 changes: 50 additions & 8 deletions packages/status-js/src/utils/encode-url-data.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import { base64url } from '@scure/base'
import { brotliCompressSync, brotliDecompressSync } from 'zlib'
import { z } from 'zod'

import { Channel, Community, URLData, User } from '../protos/url_pb'

import type { PlainMessage } from '@bufbuild/protobuf'

export type EncodedURLData = string & { _: 'EncodedURLData' }

const colorSchema = z.string().regex(/^#[0-9A-Fa-f]{6}$/)
const communityDisplayName = z.string().max(30).nonempty()

const communitySchema = z.object({
displayName: communityDisplayName,
description: z.string().max(140).nonempty(),
membersCount: z.number().nonnegative(),
color: colorSchema,
tagIndices: z.number().nonnegative().array(),
})

export function encodeCommunityURLData(
data: PlainMessage<Community>
): EncodedURLData {
Expand All @@ -17,33 +29,58 @@ export function encodeCommunityURLData(
export function decodeCommunityURLData(data: string): PlainMessage<Community> {
const deserialized = decodeURLData(data)

return Community.fromBinary(
deserialized.content
).toJson() as PlainMessage<Community>
const community = Community.fromBinary(deserialized.content).toJson()

return communitySchema.parse(community)
}

const channelSchema = z.object({
displayName: z.string().max(24).nonempty(),
description: z.string().max(140).nonempty(),
emoji: z.string().emoji(),
color: colorSchema,
community: z.object({
displayName: communityDisplayName,
}),
uuid: z.string().uuid(),
})

export function encodeChannelURLData(
data: PlainMessage<Channel>
): EncodedURLData {
return encodeURLData(new Channel(data).toBinary()) as EncodedURLData
}

export function decodeChannelURLData(data: string): PlainMessage<Channel> {
export function decodeChannelURLData(data: string): Omit<
PlainMessage<Channel>,
'community'
> & {
community: Pick<PlainMessage<Community>, 'displayName'>
} {
const deserialized = decodeURLData(data)

return Channel.fromBinary(
deserialized.content
).toJson() as PlainMessage<Channel>
const channel = Channel.fromBinary(deserialized.content).toJson()

return channelSchema.parse(channel)
}

const userSchema = z.object({
displayName: z.string().max(24).nonempty(),
description: z.string().max(240).nonempty(),
// fixme: await integration in native platforms
color: colorSchema.optional().default('#ffffff'),
})

export function encodeUserURLData(data: PlainMessage<User>): EncodedURLData {
return encodeURLData(new User(data).toBinary()) as EncodedURLData
}

export function decodeUserURLData(data: string): PlainMessage<User> {
const deserialized = decodeURLData(data)

return User.fromBinary(deserialized.content).toJson() as PlainMessage<User>
const user = User.fromBinary(deserialized.content).toJson()

return userSchema.parse(user)
}

function encodeURLData(data: Uint8Array): string {
Expand All @@ -57,6 +94,11 @@ function encodeURLData(data: Uint8Array): string {
}

function decodeURLData(data: string): URLData {
// note: https://github.com/status-im/status-web/pull/345#discussion_r1113129396 observed lengths
// note?: https://docs.google.com/spreadsheets/d/1JD4kp0aUm90piUZ7FgM_c2NGe2PdN8BFB11wmt5UZIY/view#gid=1260088614 limit for url path segmets not split by ";" or "_"
// fixme: set to 301 per url path segment when the above mentioned splitting is implemented
z.string().max(500).parse(data) // default max in order not to compute arbitrary values

const decoded = base64url.decode(data)
const decompressed = brotliDecompressSync(decoded)
const deserialized = URLData.fromBinary(decompressed)
Expand Down

0 comments on commit f9af5e0

Please sign in to comment.