Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
M
MetaGer Keymanager
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Releases
Package registry
Container Registry
Model registry
Operate
Environments
Terraform modules
Monitor
Incidents
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
open-source
MetaGer Keymanager
Commits
e333f3d0
Commit
e333f3d0
authored
2 years ago
by
Dominik Hebeler
Browse files
Options
Downloads
Plain Diff
Merge branch 'main' of gitlab.metager.de:open-source/metager-keymanager
parents
169c06aa
5c630874
No related branches found
Branches containing commit
No related tags found
No related merge requests found
Pipeline
#7292
passed
2 years ago
Stage: build
Stage: deploy
Changes
3
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
docs/api.md
+63
-0
63 additions, 0 deletions
docs/api.md
pass/app/Crypto.js
+33
-11
33 additions, 11 deletions
pass/app/Crypto.js
pass/routes/api.js
+296
-58
296 additions, 58 deletions
pass/routes/api.js
with
392 additions
and
69 deletions
docs/api.md
+
63
−
0
View file @
e333f3d0
...
@@ -139,6 +139,7 @@ This Method does not require authentication
...
@@ -139,6 +139,7 @@ This Method does not require authentication
Signs submitted blinded tokens with the servers private key. The submitted key needs to hold a charge for each token or the request will fail.
Signs submitted blinded tokens with the servers private key. The submitted key needs to hold a charge for each token or the request will fail.
The Key will be discharged by this action.
The Key will be discharged by this action.
You cannot submit more than 10 tokens to be signed.
You cannot submit more than 10 tokens to be signed.
This Method does not require authentication
### Parameters
### Parameters
...
@@ -169,3 +170,65 @@ If the key isn't charged enough for the amount of tokens or if the date supplied
...
@@ -169,3 +170,65 @@ If the key isn't charged enough for the amount of tokens or if the date supplied
}
}
}
}
```
```
## `POST /api/json/token/check`
Checks supplied tokens for validity
You cannot submit more than 10 tokens to be used.
### Parameters
```
json
{
"tokens"
:
[
{
"token"
:
<TOKEN>
,
"signature"
:
<SIGNATURE>
,
"date"
:
<DATE_AS_SUPPLIED_IN_TOKEN_PUBKEY>
,
}
...
]
}
```
### Example Response
If all tokens were used successfully response code will be
`201`
.
If any validation errors (signature, token format, etc.) occured response code will be
`422`
```
json
{
TODO
}
```
## `POST /api/json/token/use`
Uses supplied Tokens to be consumed for MetaGer search. All supplied Tokens will be invalid after this action.
You cannot submit more than 10 tokens to be used.
### Parameters
```
json
{
"tokens"
:
[
{
"token"
:
<TOKEN>
,
"signature"
:
<SIGNATURE>
,
"date"
:
<DATE_AS_SUPPLIED_IN_TOKEN_PUBKEY>
,
}
...
]
}
```
### Example Response
If all tokens were used successfully response code will be
`201`
.
If any validation errors (signature, token format, etc.) occured response code will be
`422`
```
json
{
TODO
}
```
This diff is collapsed.
Click to expand it.
pass/app/Crypto.js
+
33
−
11
View file @
e333f3d0
...
@@ -10,7 +10,6 @@ const RedisClient = require("./RedisClient");
...
@@ -10,7 +10,6 @@ const RedisClient = require("./RedisClient");
class
Crypto
{
class
Crypto
{
#dayjs
;
#dayjs
;
#dayjs_format
;
#dayjs_format
;
#redis
;
constructor
()
{
constructor
()
{
this
.
#dayjs
=
require
(
"
dayjs
"
);
this
.
#dayjs
=
require
(
"
dayjs
"
);
...
@@ -23,7 +22,7 @@ class Crypto {
...
@@ -23,7 +22,7 @@ class Crypto {
* of the given date
* of the given date
*
*
* @param {dayjs.Dayjs} date Date/Month for the private Key seed
* @param {dayjs.Dayjs} date Date/Month for the private Key seed
* @returns {NodeRSA}
* @returns {
Promise<
NodeRSA
>
}
*/
*/
async
get_private_key
(
date
)
{
async
get_private_key
(
date
)
{
let
cache_key
=
"
private_key:
"
+
date
.
format
(
this
.
#dayjs_format
);
let
cache_key
=
"
private_key:
"
+
date
.
format
(
this
.
#dayjs_format
);
...
@@ -71,16 +70,41 @@ class Crypto {
...
@@ -71,16 +70,41 @@ class Crypto {
}
}
/**
/**
*
*
* @param {String} blinded_token
* @param {String} blinded_token
* @param {NodeRSA} private_key
* @param {NodeRSA} private_key
* @returns {BigInteger}
* @returns {
Promise<
BigInteger
>
}
*/
*/
sign
(
blinded_token
,
private_key
)
{
async
sign
(
blinded_token
,
private_key
)
{
return
BlindSignature
.
sign
({
let
min_ms
=
150
;
let
start
=
dayjs
();
let
blinded_signature
=
BlindSignature
.
sign
({
blinded
:
new
BigInteger
(
blinded_token
),
blinded
:
new
BigInteger
(
blinded_token
),
key
:
private_key
key
:
private_key
,
});
});
let
missing_ms
=
Math
.
max
(
min_ms
-
dayjs
().
diff
(
start
,
"
millisecond
"
),
0
);
await
new
Promise
((
resolve
)
=>
setTimeout
(
resolve
,
missing_ms
));
return
blinded_signature
;
}
/**
*
* @param {String} token
* @param {String} date
*/
async
validateToken
(
token
,
signature
,
date
)
{
let
min_ms
=
150
;
let
start
=
dayjs
();
let
current_date
=
dayjs
(
date
,
this
.
#dayjs_format
);
let
private_key
=
await
this
.
get_private_key
(
current_date
);
let
verification_result
=
BlindSignature
.
verify2
({
unblinded
:
signature
,
key
:
private_key
,
message
:
token
,
});
let
missing_ms
=
Math
.
max
(
min_ms
-
dayjs
().
diff
(
start
,
"
millisecond
"
),
0
);
await
new
Promise
((
resolve
)
=>
setTimeout
(
resolve
,
missing_ms
));
return
verification_result
;
}
}
async
validateMetaGerPassCode
(
async
validateMetaGerPassCode
(
...
@@ -129,8 +153,6 @@ class Crypto {
...
@@ -129,8 +153,6 @@ class Crypto {
}
}
}
}
/**
/**
* Creates an hmac hash for purchase data so we can check it later
* Creates an hmac hash for purchase data so we can check it later
*/
*/
...
...
This diff is collapsed.
Click to expand it.
pass/routes/api.js
+
296
−
58
View file @
e333f3d0
var
express
=
require
(
"
express
"
);
var
express
=
require
(
"
express
"
);
var
router
=
express
.
Router
();
var
router
=
express
.
Router
();
const
{
const
{
body
,
validationResult
}
=
require
(
"
express-validator
"
);
body
,
validationResult
}
=
require
(
"
express-validator
"
);
const
config
=
require
(
"
config
"
);
const
config
=
require
(
"
config
"
);
const
Key
=
require
(
"
../app/Key
"
);
const
Key
=
require
(
"
../app/Key
"
);
...
@@ -13,26 +10,9 @@ const Manual = require("../app/payment_processor/Manual");
...
@@ -13,26 +10,9 @@ const Manual = require("../app/payment_processor/Manual");
const
Crypto
=
require
(
"
../app/Crypto
"
);
const
Crypto
=
require
(
"
../app/Crypto
"
);
const
dayjs
=
require
(
"
dayjs
"
);
const
dayjs
=
require
(
"
dayjs
"
);
const
NodeRSA
=
require
(
"
node-rsa
"
);
const
NodeRSA
=
require
(
"
node-rsa
"
);
const
RedisClient
=
require
(
"
../app/RedisClient
"
);
router
.
use
(
"
/key
"
,
(
req
,
res
,
next
)
=>
{
router
.
use
(
"
/key
"
,
authorizedOnly
);
let
auth_token
=
req
.
get
(
"
Authorization
"
);
let
authorized
=
false
;
if
(
auth_token
)
{
auth_token
=
auth_token
.
replace
(
/^Bearer /
,
""
);
let
required_token
=
config
.
get
(
"
app.api_token
"
);
if
(
required_token
===
auth_token
)
{
authorized
=
true
;
}
}
if
(
!
authorized
)
{
res
.
status
(
401
).
json
({
code
:
401
,
error
:
"
You are not authorized for API usage
"
,
});
}
else
{
next
();
}
});
router
.
post
(
"
/key/create
"
,
async
(
req
,
res
)
=>
{
router
.
post
(
"
/key/create
"
,
async
(
req
,
res
)
=>
{
let
amount
=
req
.
body
.
amount
;
let
amount
=
req
.
body
.
amount
;
...
@@ -187,21 +167,40 @@ router.get("/token/pubkey", async (req, res) => {
...
@@ -187,21 +167,40 @@ router.get("/token/pubkey", async (req, res) => {
});
});
});
});
router
.
post
(
"
/token/sign
"
,
body
(
"
key
"
).
notEmpty
(),
body
(
"
date
"
).
notEmpty
().
custom
(
value
=>
{
router
.
post
(
// Make sure the date is either for the last month or this month
"
/token/sign
"
,
let
date
=
dayjs
(
value
,
config
.
get
(
"
crypto.private_key.date_format
"
));
body
(
"
key
"
).
notEmpty
(),
let
min
=
dayjs
().
millisecond
(
0
).
second
(
0
).
minute
(
0
).
hour
(
0
).
date
(
1
).
subtract
(
1
,
"
month
"
);
body
(
"
date
"
)
let
max
=
min
.
add
(
2
,
"
month
"
);
.
notEmpty
()
if
(
!
date
.
isValid
()
||
date
.
isBefore
(
min
)
||
!
date
.
isBefore
(
max
))
{
.
custom
((
value
)
=>
{
return
Promise
.
reject
(
"
Submitted Date format is invalid
"
);
// Make sure the date is either for the last month or this month
}
let
date
=
dayjs
(
value
,
config
.
get
(
"
crypto.private_key.date_format
"
));
return
true
;
let
min
=
dayjs
()
}),
.
millisecond
(
0
)
body
(
"
blinded_tokens
"
).
notEmpty
().
withMessage
(
"
Blinded Tokens need to be defined
"
)
.
second
(
0
)
.
isArray
({
min
:
1
,
max
:
10
}).
withMessage
(
"
You can supply between 1 and 10 tokens to sign
"
)
.
minute
(
0
)
.
custom
(
value
=>
{
.
hour
(
0
)
.
date
(
1
)
.
subtract
(
1
,
"
month
"
);
let
max
=
min
.
add
(
2
,
"
month
"
);
if
(
!
date
.
isValid
()
||
date
.
isBefore
(
min
)
||
!
date
.
isBefore
(
max
))
{
return
Promise
.
reject
(
"
Submitted Date format is invalid
"
);
}
return
true
;
}),
body
(
"
blinded_tokens
"
)
.
notEmpty
()
.
withMessage
(
"
Blinded Tokens need to be defined
"
)
.
isArray
({
min
:
1
,
max
:
10
})
.
withMessage
(
"
You can supply between 1 and 10 tokens to sign
"
)
.
custom
((
value
)
=>
{
let
checked_tokens
=
{};
let
checked_tokens
=
{};
for
(
let
i
=
0
;
i
<
value
.
length
;
i
++
)
{
for
(
let
i
=
0
;
i
<
value
.
length
;
i
++
)
{
if
(
!
(
typeof
value
[
i
]
==
"
string
"
)
||
value
[
i
].
legnth
>
1024
)
{
return
Promise
.
reject
(
"
The supplied tokens can't be more than 1024 characters long
"
);
}
if
(
value
[
i
]
in
checked_tokens
)
{
if
(
value
[
i
]
in
checked_tokens
)
{
return
Promise
.
reject
(
"
The supplied tokens need to be unique
"
);
return
Promise
.
reject
(
"
The supplied tokens need to be unique
"
);
}
else
{
}
else
{
...
@@ -209,41 +208,111 @@ router.post("/token/sign", body("key").notEmpty(), body("date").notEmpty().custo
...
@@ -209,41 +208,111 @@ router.post("/token/sign", body("key").notEmpty(), body("date").notEmpty().custo
}
}
}
}
return
true
;
return
true
;
})
})
,
,
async
(
req
,
res
)
=>
{
async
(
req
,
res
)
=>
{
const
errors
=
validationResult
(
req
);
const
errors
=
validationResult
(
req
);
if
(
!
errors
.
isEmpty
())
{
if
(
!
errors
.
isEmpty
())
{
res
.
status
(
422
).
json
(
errors
);
res
.
status
(
422
).
json
(
errors
);
return
;
return
;
}
}
let
key
=
await
Key
.
GET_KEY
(
req
.
body
.
key
,
false
);
let
date
=
dayjs
(
req
.
body
.
date
,
config
.
get
(
"
crypto.private_key.date_format
"
));
let
date
=
dayjs
(
req
.
body
.
date
,
config
.
get
(
"
crypto.private_key.date_format
"
)
);
let
blinded_tokens
=
req
.
body
.
blinded_tokens
;
let
blinded_tokens
=
req
.
body
.
blinded_tokens
;
let
signed_tokens
=
{};
let
signed_tokens
=
{};
let
crypto
=
new
Crypto
();
let
key
=
await
Key
.
GET_KEY
(
req
.
body
.
key
,
true
);
crypto
.
get_private_key
(
date
).
then
(
private_key
=>
{
if
(
key
.
get_charge
()
<
blinded_tokens
.
length
)
{
for
(
let
i
=
0
;
i
<
blinded_tokens
.
length
;
i
++
)
{
await
key
.
save
();
let
blinded_token
=
blinded_tokens
[
i
];
res
.
status
(
422
).
json
({
signed_tokens
[
blinded_token
]
=
crypto
.
sign
(
blinded_token
,
private_key
).
toString
();
message
:
"
Invalid Key
"
,
}
res
.
status
(
201
).
json
({
key
:
key
.
get_key
,
discharged
:
blinded_tokens
.
length
,
date
:
date
.
format
(
config
.
get
(
"
crypto.private_key.date_format
"
)),
signed_tokens
:
signed_tokens
});
});
}).
catch
(
reason
=>
{
return
;
console
.
error
(
reason
);
}
else
{
res
.
status
(
500
).
json
({
key
.
discharge_key
(
blinded_tokens
.
length
);
status
:
500
,
await
key
.
save
();
message
:
"
Couldn't load private key
"
}
let
crypto
=
new
Crypto
();
await
crypto
.
get_private_key
(
date
)
.
then
(
async
(
private_key
)
=>
{
for
(
let
i
=
0
;
i
<
blinded_tokens
.
length
;
i
++
)
{
let
blinded_token
=
blinded_tokens
[
i
];
let
signature
=
await
crypto
.
sign
(
blinded_token
,
private_key
);
signed_tokens
[
blinded_token
]
=
signature
.
toString
();
}
})
.
catch
((
reason
)
=>
{
console
.
error
(
reason
);
res
.
status
(
500
).
json
({
status
:
500
,
message
:
"
Couldn't load private key
"
,
});
return
;
});
});
res
.
status
(
201
).
json
({
key
:
key
.
get_key
,
discharged
:
blinded_tokens
.
length
,
date
:
date
.
format
(
config
.
get
(
"
crypto.private_key.date_format
"
)),
signed_tokens
:
signed_tokens
,
});
});
}
);
router
.
post
(
"
/token/check
"
,
authorizedOnly
,
body
(
"
tokens
"
)
.
notEmpty
()
.
withMessage
(
"
Tokens need to be defined
"
)
.
bail
()
.
isArray
({
min
:
1
,
max
:
10
})
.
withMessage
(
"
You can supply between 1 and 10 tokens
"
)
.
bail
()
.
custom
((
value
)
=>
validateTokenStructure
(
value
))
.
bail
()
.
custom
(
async
(
value
)
=>
validateTokenSignature
(
value
,
false
)),
async
(
req
,
res
)
=>
{
const
errors
=
validationResult
(
req
);
if
(
!
errors
.
isEmpty
())
{
res
.
status
(
422
).
json
(
errors
);
return
;
}
res
.
json
({
status
:
"
OK
"
,
});
}
);
});
router
.
post
(
"
/token/use
"
,
authorizedOnly
,
body
(
"
tokens
"
)
.
notEmpty
()
.
withMessage
(
"
Tokens need to be defined
"
)
.
bail
()
.
isArray
({
min
:
1
,
max
:
10
})
.
withMessage
(
"
You can supply between 1 and 10 tokens
"
)
.
bail
()
.
custom
((
value
)
=>
validateTokenStructure
(
value
))
.
bail
()
.
custom
(
async
(
value
)
=>
validateTokenSignature
(
value
,
true
)),
async
(
req
,
res
)
=>
{
const
errors
=
validationResult
(
req
);
if
(
!
errors
.
isEmpty
())
{
res
.
status
(
422
).
json
(
errors
);
return
;
}
res
.
status
(
201
).
json
({
status
:
"
OK
"
,
});
}
);
router
.
use
((
req
,
res
)
=>
{
router
.
use
((
req
,
res
)
=>
{
res
.
status
(
404
).
json
({
res
.
status
(
404
).
json
({
...
@@ -252,4 +321,173 @@ router.use((req, res) => {
...
@@ -252,4 +321,173 @@ router.use((req, res) => {
});
});
});
});
function
authorizedOnly
(
req
,
res
,
next
)
{
let
auth_token
=
req
.
get
(
"
Authorization
"
);
let
authorized
=
false
;
if
(
auth_token
)
{
auth_token
=
auth_token
.
replace
(
/^Bearer /
,
""
);
let
required_token
=
config
.
get
(
"
app.api_token
"
);
if
(
required_token
===
auth_token
)
{
authorized
=
true
;
}
}
if
(
!
authorized
)
{
res
.
status
(
401
).
json
({
code
:
401
,
error
:
"
You are not authorized for API usage
"
,
});
}
else
{
next
();
}
}
async
function
validateTokenStructure
(
value
)
{
// Validate Tokens are each unique and in expected format
let
checked_tokens
=
{};
let
error
=
false
;
for
(
let
i
=
0
;
i
<
value
.
length
;
i
++
)
{
let
token
=
value
[
i
];
if
(
!
(
"
token
"
in
token
||
typeof
token
.
token
!=
"
string
"
))
{
error
=
true
;
value
[
i
][
"
status
"
]
=
"
field_missing_token
"
;
continue
;
}
if
(
!
(
"
signature
"
in
token
||
typeof
token
.
signature
!=
"
string
"
))
{
error
=
true
;
value
[
i
][
"
status
"
]
=
"
field_missing_signature
"
;
continue
;
}
if
(
!
(
"
date
"
in
token
||
typeof
token
.
date
!=
"
string
"
))
{
error
=
true
;
value
[
i
][
"
status
"
]
=
"
field_missing_date
"
;
continue
;
}
if
(
!
token
.
token
.
match
(
/^
[
0-9a-f
]{8}
-
[
0-9a-f
]{4}
-4
[
0-9a-f
]{3}
-
[
0-9a-f
]{4}
-
[
0-9a-f
]{12}
$/
)
)
{
error
=
true
;
value
[
i
][
"
status
"
]
=
"
format_invalid_token
"
;
continue
;
}
// Validate supplied expiration date
let
date
=
dayjs
(
token
.
date
,
config
.
get
(
"
crypto.private_key.date_format
"
));
let
min
=
dayjs
()
.
millisecond
(
0
)
.
second
(
0
)
.
minute
(
0
)
.
hour
(
0
)
.
date
(
1
)
.
subtract
(
1
,
"
month
"
);
let
max
=
min
.
add
(
2
,
"
month
"
);
if
(
!
token
.
date
.
match
(
/^
\d{4}
-
(
0
[
1-9
]
|1
[
1-2
])
$/
)
||
!
date
.
isValid
()
||
date
.
isBefore
(
min
)
||
!
date
.
isBefore
(
max
)
)
{
error
=
true
;
value
[
i
][
"
status
"
]
=
"
format_invalid_date
"
;
continue
;
}
if
(
!
token
.
signature
.
match
(
/^
\d
+$/
)
||
token
.
signature
.
length
>
1024
)
{
error
=
true
;
value
[
i
][
"
status
"
]
=
"
format_invalid_signature
"
;
continue
;
}
if
(
token
.
token
in
checked_tokens
)
{
error
=
true
;
value
[
i
][
"
status
"
]
=
"
token_not_unique
"
;
continue
;
}
else
{
checked_tokens
[
token
.
token
]
=
true
;
}
}
if
(
error
)
{
return
Promise
.
reject
(
"
Token structure is invalid
"
);
}
return
true
;
}
async
function
validateTokenSignature
(
value
,
mark_used
=
false
)
{
// Now that we checked Format of tokens: Check the signature
let
crypto
=
new
Crypto
();
let
error
=
false
;
let
redis_client
=
RedisClient
.
CLIENT
();
let
used_tokens
=
{};
for
(
let
i
=
0
;
i
<
value
.
length
;
i
++
)
{
let
token
=
value
[
i
];
// Check if token was already used
let
redis_hash_key
=
"
tokens:used:
"
+
token
.
date
;
if
(
await
redis_client
.
hexists
(
redis_hash_key
,
token
.
token
))
{
error
=
true
;
value
[
i
][
"
status
"
]
=
"
token_already_used
"
;
continue
;
}
let
verification_result
=
await
crypto
.
validateToken
(
token
.
token
,
token
.
signature
,
token
.
date
);
if
(
!
verification_result
)
{
value
[
i
][
"
status
"
]
=
"
invalid_signature
"
;
error
=
true
;
}
else
{
value
[
i
][
"
status
"
]
=
"
ok
"
;
// Tokens can be used. Store the used tokens
if
(
mark_used
)
{
let
redis_hash_expiration
=
dayjs
()
.
add
(
2
,
"
month
"
)
.
date
(
1
)
.
hour
(
0
)
.
minute
(
0
)
.
second
(
0
)
.
millisecond
(
0
);
let
usage_success
=
await
redis_client
.
pipeline
()
.
hsetnx
(
redis_hash_key
,
token
.
token
,
dayjs
().
format
(
"
YYYY-MM-DD HH:mm:ss
"
)
)
.
expireat
(
redis_hash_key
,
redis_hash_expiration
.
unix
())
.
exec
();
if
(
usage_success
===
0
)
{
error
=
true
;
value
[
i
][
"
status
"
]
=
"
token_already_used
"
;
}
else
{
// Store used tokens in memory so we can rollback in case of later error
if
(
!
(
redis_hash_key
in
used_tokens
))
{
used_tokens
[
redis_hash_key
]
=
[];
}
used_tokens
[
redis_hash_key
].
push
(
token
.
token
);
}
}
}
}
if
(
error
)
{
if
(
mark_used
)
{
// Rollback any used tokens
for
(
let
redis_hash_key
in
used_tokens
)
{
await
redis_client
.
hdel
(
redis_hash_key
,
used_tokens
[
redis_hash_key
]);
}
}
await
redis_client
.
quit
();
return
Promise
.
reject
(
"
Invalid Signatures
"
);
}
else
{
await
redis_client
.
quit
();
}
return
true
;
}
module
.
exports
=
router
;
module
.
exports
=
router
;
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment