Assuming you are a developer, you probably ran into the need to access some secret from your app. For example a user/password/key to connect a remote system.
In SAP BTP you of course have the destination service that cover some use cases with a good security approach.
Unfortunately the destination service does not fit all needs and sometimes you need to store and access secrets.
Letβs say we have this βmyVerySecretMessageβ string that we need to access from the application run time.
So what can we do:
- Store it directly in the code β BAD ideaΒ πΒ it can be access by anyone with access to your code, plus it may leaks to your git repo and you donβt want that (tip: open source tool that checks your git repo for hardcoded secrets-Β https://github.com/zricethezav/gitleaksΒ )
- Store it as an environment variable β this is definitely better then storing it hardcoded in the code, but still has its soft belly
- It is stored as plan text
- No security around it β any one that can access the env can see it
- Might be exposed in log files like history file
- Finally, we can use the βCredential Storeβ service in the BTPΒ π€π€π€
- Secured very well
- All data is Encrypted
Let me share with you the small experience I have with that nice BTP service:
- Make sure your space has the entitlement to use this service
- From the left navigation menu, chooseΒ ServicesΒ > Service Marketplace > Choose theΒ Credential StoreΒ tile
- During service creation :
- you should choose the plan (in my config I choose βstandardβ plan)
- give it a name e.g. βmycredstoreβ
- Bind the service instance to your application (can be done from BTP cockpit or from the terminal)
- The terminal way: cf bind-service myapp mycredstore
- After a success binding, you can find some credstore connection props via the VCAP_SERVICES environment variable
- username: this property is used for authentication. It contains the unique binding ID.
- private_key: this property contains the private key used to sign JWT tokens for authentication. See: Encrypting Payloads
- oauth_token_url: SAP Credential Store supports OAuth client credentials flow for authentication. This property contains the token URL of the OAuth authorization server, which accepts either signed JWT tokens, or username/password and issues access tokens.
- url: this property contains the URL of the SAP Credential Store REST API. The URL can be accessed only with a valid access token issued by the OAuth authorization server (see property oauth_token_url).
- parameters: this property contains either the default binding options, or the custom ones defined during the binding creation. Currently, it contains only an authorization section with access permissions.
Now we come to the application integration, Iβm showing the credstore api from a CAP application based on nodeJS:
I modified a bit the Credential Management βΒ SAP Doc Example (Node.js)Β so it can be called with the require
some thing like this:
const {readCredential, writeCredential, deleteCredential} = require('./utils/cred')
Here is the modified cred.js api that Β gives the read/create/update/delete Β credential elements from the credstore (again for the original βΒ SAP Doc Example (Node.js)):
const jose = require('node-jose');
const fetch = require('node-fetch'); // use version 2 when working with commonJS modules
const xsenv = require("@sap/xsenv");
xsenv.loadEnv();
const binding = xsenv.getServices({ credstore: { tag: 'credstore' } }).credstore;
function checkStatus(response) {
if (!response.ok) console.log(`Please verify that you CredStore binding info is up to date
Unexpected status code: ${response.status}`);
return response;
}
async function decryptPayload(privateKey, payload) {
const key = await jose.JWK.asKey(`-----BEGIN PRIVATE KEY-----${privateKey}-----END PRIVATE KEY-----`,
"pem",
{ alg: "RSA-OAEP-256", enc: "A256GCM" }
);
const decrypt = await jose.JWE.createDecrypt(key).decrypt(payload);
const result = decrypt.plaintext.toString();
return result;
}
async function encryptPayload(publicKey, payload) {
const key = await jose.JWK.asKey(`-----BEGIN PUBLIC KEY-----${publicKey}-----END PUBLIC KEY-----`,
"pem",
{ alg: "RSA-OAEP-256" }
);
const options = {
contentAlg: "A256GCM",
compact: true,
fields: { "iat": Math.round(new Date().getTime() / 1000) }
};
return jose.JWE.createEncrypt(options, key).update(Buffer.from(payload, "utf8")).final();
}
function headers(binding, namespace, init) {
const headers = new fetch.Headers(init);
headers.set("Authorization", `Basic ${Buffer.from(`${binding.username}:${binding.password}`).toString("base64")}`);
headers.set("sapcp-credstore-namespace", namespace);
return headers;
}
async function fetchAndDecrypt(privateKey, url, method, headers, body) {
return fetch(url, { method, headers, body })
.then((resFetch)=>{
const resCheckStatus = checkStatus(resFetch)
return(resCheckStatus)})
.then((response) => {
const resText = response.text()
return resText
})
.then(payload => decryptPayload(privateKey, payload))
.then((resObj)=>{
return(JSON.parse(resObj))})
.catch(e=>{
`opps :( we have an issue: ${JSON.stringify(e)}`
});
}
async function readCredential(namespace, type, name) {
const headersToSend = headers(binding, namespace)
const resData = await fetchAndDecrypt(
binding.encryption.client_private_key,
`${binding.url}/${type}?name=${encodeURIComponent(name)}`,
"get",
headersToSend
);
return(resData)
}
async function writeCredential( namespace, type, credential) {
const headersToSend = headers(binding, namespace, { "Content-Type": "application/jose" })
const bodyToSend = await encryptPayload(binding.encryption.server_public_key, JSON.stringify(credential))
const resData = await fetchAndDecrypt(
binding.encryption.client_private_key,
`${binding.url}/${type}`,
"post",
headersToSend,
bodyToSend
);
return(resData)
}
async function deleteCredential(namespace, type, name) {
await fetch(
`${binding.url}/${type}?name=${encodeURIComponent(name)}`,
{
method: "delete",
headers: headers(binding, namespace)
}
).then(checkStatus);
}
module.exports = {readCredential, writeCredential, deleteCredential}
Now after calling this module, we can do all CRUD functionality on the Credential Store, for example:
app.get('/testCredStore',async(req,res)=>{
const {readCredential, writeCredential, deleteCredential} = require('./utils/cred')
const testItem = {
name: "password1",
value: "myVerySecretMessage",
username: "user1",
metadata: "if you see this text, the cred store is working :)"
};
const writeRes = await writeCredential("test-ns", "password", testItem)
const readRes = await readCredential("test-ns", "password", "password1")
testItem.value="secret2"
await writeCredential("test-ns", "password", testItem)
const readAfterUpdate = await readCredential("test-ns", "password", "password1")
await deleteCredential("test-ns", "password", "password1")
res.json({writeRes, readRes, readAfterUpdate})
})
Finally! We can try to see if it all works. this is the result I got on the browser:
// http://localhost:4004/testCredStore
{
"writeRes": {
"id": "95dd3b54-0e3f-40e5-8a9f-517a01df5dd1",
"name": "password1",
"modifiedAt": "2022-06-27T12:38:08.540Z",
"metadata": "if you see this text, the cred store is working :)",
"status": "enabled",
"username": "user1",
"type": "password"
},
"readRes": {
"id": "95dd3b54-0e3f-40e5-8a9f-517a01df5dd1",
"name": "password1",
"modifiedAt": "2022-06-27T12:38:08.540Z",
"metadata": "if you see this text, the cred store is working :)",
"value": "myVerySecretMessage",
"status": "enabled",
"username": "user1",
"type": "password"
},
"readAfterUpdate": {
"id": "19e64f55-a6b6-48dc-bd36-db638ef610cd",
"name": "password1",
"modifiedAt": "2022-06-27T12:38:09.688Z",
"metadata": "if you see this text, the cred store is working :)",
"value": "secret2",
"status": "enabled",
"username": "user1",
"type": "password"
}
}
YAY Β ππΒ we got the secret message from the nodeJS app in a secure way
More information about the SAP Cloud Platform Credential Store can be found here in the SAP Help Documentation:Β Credential Store β SAP Help Portal