@model-ts/dynamodb 0.2.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.ts CHANGED
@@ -79,6 +79,12 @@ export interface ClientProps
79
79
  ServiceConfigurationOptions,
80
80
  ClientApiVersions {
81
81
  tableName: string
82
+
83
+ /**
84
+ * The encryption key used to encrypt cursors using AES-256-CTR.
85
+ * Must be a 32 character string, 256 bits.
86
+ */
87
+ cursorEncryptionKey?: Buffer
82
88
  }
83
89
 
84
90
  export interface Key {
@@ -90,9 +96,11 @@ export class Client {
90
96
  tableName: string
91
97
  documentClient: DocumentClient
92
98
  dataLoader: DataLoader<GetOperation<Decodable>, DynamoDBModelInstance, string>
99
+ cursorEncryptionKey?: Buffer
93
100
 
94
101
  constructor(props: ClientProps) {
95
102
  this.tableName = props?.tableName
103
+ this.cursorEncryptionKey = props?.cursorEncryptionKey
96
104
  this.documentClient = new DocumentClient(props)
97
105
  this.dataLoader = new DataLoader<
98
106
  GetOperation<Decodable>,
@@ -189,7 +197,7 @@ export class Client {
189
197
  const { Item } = await this.documentClient
190
198
  .get({
191
199
  TableName: this.tableName,
192
- Key: key,
200
+ Key: { PK: key.PK, SK: key.SK },
193
201
  ...params,
194
202
  })
195
203
  .promise()
@@ -494,7 +502,16 @@ export class Client {
494
502
  ...params,
495
503
  // Fetch one additional item to test for a next page
496
504
  Limit: limit + 1,
497
- ExclusiveStartKey: cursor ? decodeDDBCursor(cursor) : undefined,
505
+ ExclusiveStartKey: cursor
506
+ ? decodeDDBCursor(
507
+ cursor,
508
+ // GSI1 is the inverse index and uses PK and SK (switched around)
509
+ params.IndexName && params.IndexName !== "GSI1"
510
+ ? (params.IndexName as "GSI2" | "GSI3" | "GSI4" | "GSI5")
511
+ : undefined,
512
+ this.cursorEncryptionKey
513
+ )
514
+ : undefined,
498
515
  ScanIndexForward: direction === PaginationDirection.FORWARD,
499
516
  },
500
517
  { results: model }
@@ -509,13 +526,7 @@ export class Client {
509
526
  // Build edges
510
527
  const edges = slice.map((item: any) => ({
511
528
  node: item,
512
- cursor: encodeDDBCursor(
513
- item,
514
- // GSI1 is the inverse index and uses PK and SK (switched around)
515
- params.IndexName && params.IndexName !== "GSI1"
516
- ? (params.IndexName as "GSI2" | "GSI3")
517
- : undefined
518
- ),
529
+ cursor: encodeDDBCursor(item, this.cursorEncryptionKey),
519
530
  }))
520
531
 
521
532
  return {
@@ -11,6 +11,10 @@ export interface DynamoDBModelInstance extends ModelInstance<string, any> {
11
11
  GSI2SK?: string
12
12
  GSI3PK?: string
13
13
  GSI3SK?: string
14
+ GSI4PK?: string
15
+ GSI4SK?: string
16
+ GSI5PK?: string
17
+ GSI5SK?: string
14
18
  }
15
19
 
16
20
  PK: string
package/src/pagination.ts CHANGED
@@ -1,5 +1,9 @@
1
+ import crypto from "crypto"
1
2
  import { PaginationError } from "./errors"
2
3
 
4
+ const SIV = "Q05yyCR+0tyWl6glrZhlNw=="
5
+ const ENCRYPTION_ALG = "aes-256-ctr"
6
+
3
7
  export interface PageInfo {
4
8
  hasPreviousPage: boolean
5
9
  hasNextPage: boolean
@@ -71,6 +75,47 @@ export function decodePagination(pagination: PaginationInput): {
71
75
  }
72
76
  }
73
77
 
78
+ /**
79
+ * Utility function to encrypt a cursor with AES-256-CTR, but uses a
80
+ * synthetic initialization vector (SIV) to ensure that the same cursor
81
+ * produces the same encrypted value.
82
+ */
83
+ const encryptCursor = (key: Buffer, cursor: string) => {
84
+ const cipher = crypto.createCipheriv(
85
+ ENCRYPTION_ALG,
86
+ key,
87
+ Buffer.from(SIV, "base64")
88
+ )
89
+
90
+ const encrypted = Buffer.concat([cipher.update(cursor), cipher.final()])
91
+
92
+ return encrypted.toString("base64")
93
+ }
94
+
95
+ /**
96
+ * Utility function to decrypt a cursor with AES-256-CTR, but uses a
97
+ * synthetic initialization vector (SIV) to ensure that the same cursor
98
+ * produces the same encrypted value.
99
+ */
100
+ const decryptCursor = (key: Buffer, encryptedCursor: string) => {
101
+ try {
102
+ const decipher = crypto.createDecipheriv(
103
+ ENCRYPTION_ALG,
104
+ key,
105
+ Buffer.from(SIV, "base64")
106
+ )
107
+
108
+ const decrypted = Buffer.concat([
109
+ decipher.update(Buffer.from(encryptedCursor, "base64")),
110
+ decipher.final(),
111
+ ]).toString()
112
+
113
+ return decrypted
114
+ } catch (error) {
115
+ return null
116
+ }
117
+ }
118
+
74
119
  export const encodeDDBCursor = (
75
120
  {
76
121
  PK,
@@ -95,21 +140,10 @@ export const encodeDDBCursor = (
95
140
  GSI5PK?: string
96
141
  GSI5SK?: string
97
142
  },
98
- index?: "GSI2" | "GSI3" | "GSI4" | "GSI5"
99
- ) =>
100
- index === "GSI2"
101
- ? Buffer.from(JSON.stringify({ PK, SK, GSI2PK, GSI2SK })).toString("base64")
102
- : index === "GSI3"
103
- ? Buffer.from(JSON.stringify({ PK, SK, GSI3PK, GSI3SK })).toString("base64")
104
- : index === "GSI4"
105
- ? Buffer.from(JSON.stringify({ PK, SK, GSI4PK, GSI4SK })).toString("base64")
106
- : index === "GSI5"
107
- ? Buffer.from(JSON.stringify({ PK, SK, GSI5PK, GSI5SK })).toString("base64")
108
- : Buffer.from(JSON.stringify({ PK, SK })).toString("base64")
109
-
110
- export const decodeDDBCursor = (encoded: string) => {
111
- try {
112
- const {
143
+ encryptionKey?: Buffer
144
+ ) => {
145
+ const cursor = Buffer.from(
146
+ JSON.stringify({
113
147
  PK,
114
148
  SK,
115
149
  GSI2PK,
@@ -120,11 +154,26 @@ export const decodeDDBCursor = (encoded: string) => {
120
154
  GSI4SK,
121
155
  GSI5PK,
122
156
  GSI5SK,
123
- } = JSON.parse(Buffer.from(encoded, "base64").toString())
157
+ })
158
+ ).toString("base64")
124
159
 
125
- if (typeof PK !== "string" || typeof SK !== "string") throw new Error()
160
+ if (encryptionKey) return encryptCursor(encryptionKey, cursor)
161
+
162
+ return cursor
163
+ }
164
+
165
+ export const decodeDDBCursor = (
166
+ encoded: string,
167
+ index?: "GSI2" | "GSI3" | "GSI4" | "GSI5",
168
+ encryptionKey?: Buffer
169
+ ) => {
170
+ try {
171
+ const json = encryptionKey ? decryptCursor(encryptionKey, encoded) : encoded
172
+ // const json = encoded
173
+
174
+ if (!json) throw new Error("Couldn't decrypt cursor")
126
175
 
127
- return {
176
+ const {
128
177
  PK,
129
178
  SK,
130
179
  GSI2PK,
@@ -135,7 +184,15 @@ export const decodeDDBCursor = (encoded: string) => {
135
184
  GSI4SK,
136
185
  GSI5PK,
137
186
  GSI5SK,
138
- }
187
+ } = JSON.parse(Buffer.from(json, "base64").toString())
188
+
189
+ if (typeof PK !== "string" || typeof SK !== "string") throw new Error()
190
+
191
+ if (!index) return { PK, SK }
192
+ if (index === "GSI2") return { PK, SK, GSI2PK, GSI2SK }
193
+ if (index === "GSI3") return { PK, SK, GSI3PK, GSI3SK }
194
+ if (index === "GSI4") return { PK, SK, GSI4PK, GSI4SK }
195
+ if (index === "GSI5") return { PK, SK, GSI5PK, GSI5SK }
139
196
  } catch (error) {
140
197
  throw new PaginationError("Couldn't decode cursor")
141
198
  }
package/src/provider.ts CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  import { OutputOf, TypeOf, ModelOf } from "@model-ts/core"
18
18
  import { RaceConditionError } from "./errors"
19
19
  import { absurd } from "fp-ts/lib/function"
20
- import { PaginationInput } from "./pagination"
20
+ import { encodeDDBCursor, PaginationInput } from "./pagination"
21
21
 
22
22
  export interface DynamoDBInternals<M extends Decodable> {
23
23
  __dynamoDBDecode(
@@ -524,6 +524,9 @@ export const getProvider = (client: Client) => {
524
524
  GSI5SK: this.GSI5SK,
525
525
  }
526
526
  },
527
+ cursor<T extends DynamoDBModelInstance>(this: T) {
528
+ return encodeDDBCursor(this.keys(), client.cursorEncryptionKey)
529
+ },
527
530
  put<T extends DynamoDBModelInstance>(
528
531
  this: T,
529
532
  params?: Omit<
@@ -621,7 +624,7 @@ export const getProvider = (client: Client) => {
621
624
  return client.get<M>({
622
625
  _model: this,
623
626
  _operation: "get",
624
- key,
627
+ key: { PK: key.PK, SK: key.SK },
625
628
  ...params,
626
629
  })
627
630
  },